diff options
-rw-r--r-- | API.txt | 3 | ||||
-rw-r--r-- | VERSION | 2 | ||||
-rw-r--r-- | freeipa.spec.in | 14 | ||||
-rw-r--r-- | install/Makefile.am | 1 | ||||
-rw-r--r-- | install/configure.ac | 1 | ||||
-rw-r--r-- | install/oddjob/Makefile.am | 28 | ||||
-rwxr-xr-x | install/oddjob/com.redhat.idm.trust-fetch-domains | 198 | ||||
-rw-r--r-- | install/oddjob/etc/dbus-1/system.d/oddjob-ipa-trust.conf | 40 | ||||
-rw-r--r-- | install/oddjob/etc/oddjobd.conf.d/oddjobd-ipa-trust.conf | 21 | ||||
-rw-r--r-- | ipalib/plugins/trust.py | 141 | ||||
-rw-r--r-- | ipaserver/dcerpc.py | 44 |
11 files changed, 442 insertions, 51 deletions
@@ -4971,11 +4971,12 @@ arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=Tr option: Str('version?', exclude='webui') output: Output('result', None, None) command: trust_add -args: 1,13,3 +args: 1,14,3 arg: Str('cn', attribute=True, cli_name='realm', multivalue=False, primary_key=True, required=True) 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: Bool('bidirectional?', cli_name='two_way', default=False) 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') @@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=138 +IPA_API_VERSION_MINOR=139 # Last change: mbabinsk: Commands to manage user/host/service certificates diff --git a/freeipa.spec.in b/freeipa.spec.in index 52af50dd0..46586ed71 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -204,6 +204,7 @@ Requires: samba >= %{samba_version} Requires: samba-winbind Requires: libsss_idmap Requires: libsss_nss_idmap-python +Requires: oddjob %if (0%{?fedora} >= 22) Requires: python-sss %endif @@ -581,6 +582,8 @@ fi %post server-trust-ad %{_sbindir}/update-alternatives --install %{_libdir}/krb5/plugins/libkrb5/winbind_krb5_locator.so \ winbind_krb5_locator.so /dev/null 90 +/bin/systemctl reload-or-try-restart dbus +/bin/systemctl reload-or-try-restart oddjobd %posttrans server-trust-ad python2 -c "import sys; from ipaserver.install import installutils; sys.exit(0 if installutils.is_ipa_configured() else 1);" > /dev/null 2>&1 @@ -593,6 +596,8 @@ fi %preun server-trust-ad if [ $1 -eq 0 ]; then %{_sbindir}/update-alternatives --remove winbind_krb5_locator.so /dev/null + /bin/systemctl reload-or-try-restart dbus + /bin/systemctl reload-or-try-restart oddjobd fi %endif # ONLY_CLIENT @@ -830,6 +835,9 @@ fi %attr(755,root,root) %{plugin_dir}/libipa_otp_counter.so %attr(755,root,root) %{plugin_dir}/libipa_otp_lasttoken.so %attr(755,root,root) %{plugin_dir}/libtopology.so +%attr(755,root,root) %{plugin_dir}/libipa_sidgen.so +%attr(755,root,root) %{plugin_dir}/libipa_sidgen_task.so +%attr(755,root,root) %{plugin_dir}/libipa_extdom_extop.so %dir %{_localstatedir}/lib/ipa %attr(700,root,root) %dir %{_localstatedir}/lib/ipa/backup %attr(700,root,root) %dir %{_localstatedir}/lib/ipa/sysrestore @@ -864,15 +872,15 @@ fi %files server-trust-ad %{_sbindir}/ipa-adtrust-install -%attr(755,root,root) %{plugin_dir}/libipa_extdom_extop.so %{_usr}/share/ipa/smb.conf.empty %attr(755,root,root) %{_libdir}/samba/pdb/ipasam.so -%attr(755,root,root) %{plugin_dir}/libipa_sidgen.so -%attr(755,root,root) %{plugin_dir}/libipa_sidgen_task.so %{_mandir}/man1/ipa-adtrust-install.1.gz %{python_sitelib}/ipaserver/dcerpc* %{python_sitelib}/ipaserver/install/adtrustinstance* %ghost %{_libdir}/krb5/plugins/libkrb5/winbind_krb5_locator.so +%{_sysconfdir}/dbus-1/system.d/oddjob-ipa-trust.conf +%{_sysconfdir}/oddjobd.conf.d/oddjobd-ipa-trust.conf +%%attr(755,root,root) %{_libexecdir}/ipa/com.redhat.idm.trust-fetch-domains %endif # ONLY_CLIENT diff --git a/install/Makefile.am b/install/Makefile.am index c07f57155..ac52ad3bb 100644 --- a/install/Makefile.am +++ b/install/Makefile.am @@ -17,6 +17,7 @@ SUBDIRS = \ po \ restart_scripts \ wsgi \ + oddjob \ $(NULL) install-exec-local: diff --git a/install/configure.ac b/install/configure.ac index 57f4219b6..cf19758a1 100644 --- a/install/configure.ac +++ b/install/configure.ac @@ -103,6 +103,7 @@ AC_CONFIG_FILES([ po/Makefile restart_scripts/Makefile wsgi/Makefile + oddjob/Makefile ]) AC_OUTPUT diff --git a/install/oddjob/Makefile.am b/install/oddjob/Makefile.am new file mode 100644 index 000000000..9dde10c70 --- /dev/null +++ b/install/oddjob/Makefile.am @@ -0,0 +1,28 @@ +NULL = + +oddjobdir = $(libexecdir)/ipa +oddjobconfdir = $(sysconfdir)/oddjobd.conf.d +dbusconfdir = $(sysconfdir)/dbus-1/system.d + +oddjob_SCRIPTS = \ + com.redhat.idm.trust-fetch-domains \ + $(NULL) + +dbusconf_DATA = \ + etc/dbus-1/system.d/oddjob-ipa-trust.conf \ + $(NULL) + +oddjobconf_DATA = \ + etc/oddjobd.conf.d/oddjobd-ipa-trust.conf \ + $(NULL) + + +#EXTRA_DIST = \ +# $(oddjob_SCRIPTS) \ +# $(dbusconf_DATA) \ +# $(oddjobconf_DATA) \ +# $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + Makefile.in diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains b/install/oddjob/com.redhat.idm.trust-fetch-domains new file mode 100755 index 000000000..2571dd09a --- /dev/null +++ b/install/oddjob/com.redhat.idm.trust-fetch-domains @@ -0,0 +1,198 @@ +#!/usr/bin/python2 + +from ipaserver import dcerpc +from ipaserver.install.installutils import is_ipa_configured, ScriptError +from ipapython import config, ipautil +from ipalib import api, errors +from ipapython.dn import DN +from ipalib.config import Env +from ipalib.constants import DEFAULT_CONFIG +from ipalib.krb_utils import KRB5_CCache +import sys +import os, pwd +import krbV +import time + +# This version is different from the original in ipapyton.ipautil +# in the fact that it returns a krbV.CCache object. +def kinit_keytab(principal, keytab, ccache_name, attempts=1): + errors_to_retry = {krbV.KRB5KDC_ERR_SVC_UNAVAILABLE, + krbV.KRB5_KDC_UNREACH} + for attempt in range(1, attempts + 1): + try: + krbcontext = krbV.default_context() + ktab = krbV.Keytab(name=keytab, context=krbcontext) + princ = krbV.Principal(name=principal, context=krbcontext) + ccache = krbV.CCache(name=ccache_name, context=krbcontext, + primary_principal=princ) + ccache.init(princ) + ccache.init_creds_keytab(keytab=ktab, principal=princ) + return ccache + except krbV.Krb5Error as e: + if e.args[0] not in errors_to_retry: + raise + if attempt == attempts: + raise + time.sleep(5) + +def retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal): + getkeytab_args = ["/usr/sbin/ipa-getkeytab", + "-s", api.env.host, + "-p", oneway_principal, + "-k", oneway_keytab_name, + "-r"] + (stdout, stderr, retcode) = ipautil.run(getkeytab_args, + env={'KRB5CCNAME': ccache_name, 'LANG': 'C'}, + raiseonerr=False) + # Make sure SSSD is able to read the keytab + sssd = pwd.getpwnam('sssd') + os.chown(oneway_keytab_name, sssd[2], sssd[3]) + + +def parse_options(): + usage = "%prog <trusted domain name>\n" + parser = config.IPAOptionParser(usage=usage, + formatter=config.IPAFormatter()) + + parser.add_option("-d", "--debug", action="store_true", dest="debug", + help="Display debugging information") + + options, args = parser.parse_args() + safe_options = parser.get_safe_opts(options) + + return safe_options, options, args + + +if not is_ipa_configured(): + # LSB status code 6: program is not configured + raise ScriptError("IPA is not configured " + + "(see man pages of ipa-server-install for help)", 6) + +if not os.getegid() == 0: + # LSB status code 4: user had insufficient privilege + raise ScriptError("You must be root to run ipactl.", 4) + +safe_options, options, args = parse_options() + +if len(args) != 1: + # LSB status code 2: invalid or excess argument(s) + raise ScriptError("You must specify trusted domain name", 2) + +trusted_domain = unicode(args[0].lower()) + +env = Env() +env._bootstrap(context='server', debug=options.debug, log=None) +env._finalize_core(**dict(DEFAULT_CONFIG)) + +# Initialize the API with the proper debug level +api.bootstrap(context='server', debug=env.debug, log=None) +api.finalize() + +# Only import trust plugin after api is initialized or internal imports +# within the plugin will not work +from ipalib.plugins import trust + +# We have to dance with two different credentials caches: +# ccache_name -- for cifs/ipa.master@IPA.REALM to communicate with LDAP +# oneway_ccache_name -- for IPA$@AD.REALM to communicate with AD DCs +# +# ccache_name may not exist, we'll have to initialize it from Samba's keytab +# +# oneway_ccache_name may not exist either but to initialize it, we need +# to check if oneway_keytab_name keytab exists and fetch it first otherwise. +# +# to fetch oneway_keytab_name keytab, we need to initialize ccache_name ccache first +# and retrieve our own NetBIOS domain name and use cifs/ipa.master@IPA.REALM to +# retrieve the keys to oneway_keytab_name. + +keytab_name = '/etc/samba/samba.keytab' +oneway_keytab_name = '/var/lib/sss/keytabs/' + trusted_domain + '.keytab' + +principal = str('cifs/' + api.env.host) + +oneway_ccache_name = '/var/run/ipa/krb5cc_oddjob_trusts_fetch' +ccache_name = '/var/run/ipa/krb5cc_oddjob_trusts' + +# Standard sequence: +# - check if ccache exists +# - if not, initialize it from Samba's keytab +# - check if ccache contains valid TGT +# - if not, initialize it from Samba's keytab +# - refer the correct ccache object for further use +# +if not os.path.isfile(ccache_name): + ccache = kinit_keytab(principal, keytab_name, ccache_name) + +ccache_check = KRB5_CCache(ccache_name) +if not ccache_check.credential_is_valid(principal): + ccache = kinit_keytab(principal, keytab_name, ccache_name) +else: + ccache = ccache_check.ccache + +old_ccache = os.environ.get('KRB5CCNAME') +api.Backend.ldap2.connect(ccache) + +own_trust_dn = DN(('cn', api.env.domain),('cn','ad'), ('cn', 'etc'), api.env.basedn) +own_trust_entry = api.Backend.ldap2.get_entry(own_trust_dn, ['ipantflatname']) +own_trust_flatname = own_trust_entry['ipantflatname'][0].upper() + +oneway_principal = str('%s$@%s' % (own_trust_flatname, trusted_domain.upper())) + +# If keytab does not exist, retrieve it +if not os.path.isfile(oneway_keytab_name): + retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal) + +oneway_ccache = None +try: + # The keytab may have stale key material (from older trust-add run) + if not os.path.isfile(oneway_ccache_name): + oneway_ccache = kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) +except krbV.Krb5Error as e: + # If there was failure on using keytab, assume it is stale and retrieve again + retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal) + +if oneway_ccache: + # There wasn existing ccache, validate its content + oneway_ccache_check = KRB5_CCache(oneway_ccache_name) + if not oneway_ccache_check.credential_is_valid(oneway_principal): + # If credentials were invalid, obtain them again + oneway_ccache = kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) + else: + oneway_ccache = oneway_ccache_check.ccache +else: + oneway_ccache = kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) + +# We are done: we have ccache with TDO credentials and can fetch domains +ipa_domain = api.env.domain +os.environ['KRB5CCNAME'] = oneway_ccache_name +domains = dcerpc.fetch_domains(api, ipa_domain, trusted_domain, creds=True) + +if domains: + # trust range must exist by the time fetch_domains_from_trust is called + range_name = unicode(trusted_domain.upper() + '_id_range') + old_range = api.Command.idrange_show(range_name, raw=True)['result'] + idrange_type = old_range['iparangetype'][0] + + result = [] + for dom in domains: + dom['trust_type'] = u'ad' + try: + name = dom['cn'] + del dom['cn'] + + res = api.Command.trustdomain_add(trusted_domain, name, **dom) + result.append(res['result']) + + if idrange_type != u'ipa-ad-trust-posix': + range_name = name.upper() + '_id_range' + dom['range_type'] = u'ipa-ad-trust' + trust.add_range(range_name, dom['ipanttrusteddomainsid'], + trusted_domain, name, **dom) + except errors.DuplicateEntry: + # Ignore updating duplicate entries + pass + +if old_ccache: + os.environ['KRB5CCNAME'] = old_ccache + +sys.exit(0) diff --git a/install/oddjob/etc/dbus-1/system.d/oddjob-ipa-trust.conf b/install/oddjob/etc/dbus-1/system.d/oddjob-ipa-trust.conf new file mode 100644 index 000000000..2e4c1367b --- /dev/null +++ b/install/oddjob/etc/dbus-1/system.d/oddjob-ipa-trust.conf @@ -0,0 +1,40 @@ +<!DOCTYPE busconfig PUBLIC + "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> + +<busconfig> + <!-- Only root can own (provide) the com.redhat.idm.trust service + on the system bus. --> + <policy user="root"> + <allow own="com.redhat.idm.trust"/> + <allow send_destination="com.redhat.idm.trust" + send_path="/" + send_interface="com.redhat.idm.trust" + send_member="fetch_domains"/> + </policy> + + <!-- Allow anyone to call the introspection methods of the "/" object + provided by the com.redhat.idm.trust service. --> + <policy context="default"> + <allow send_destination="com.redhat.idm.trust" + send_path="/" + send_interface="org.freedesktop.DBus.Introspectable" + send_member="Introspect"/> + <allow send_destination="com.redhat.idm.trust" + send_path="/" + send_interface="org.freedesktop.DBus.Properties" + send_member="GetAll"/> + <allow send_destination="com.redhat.idm.trust" + send_path="/" + send_interface="org.freedesktop.DBus.Properties" + send_member="Get"/> + </policy> + + <policy user="apache"> + <allow send_destination="com.redhat.idm.trust" + send_path="/" + send_interface="com.redhat.idm.trust" + send_member="fetch_domains"/> + </policy> + +</busconfig> diff --git a/install/oddjob/etc/oddjobd.conf.d/oddjobd-ipa-trust.conf b/install/oddjob/etc/oddjobd.conf.d/oddjobd-ipa-trust.conf new file mode 100644 index 000000000..17817de09 --- /dev/null +++ b/install/oddjob/etc/oddjobd.conf.d/oddjobd-ipa-trust.conf @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<oddjobconfig> + <service name="com.redhat.idm.trust"> + <allow user="root"/> + <allow user="apache"/> + <object name="/"> + <interface name="org.freedesktop.DBus.Introspectable"> + <allow min_uid="0" max_uid="0"/> + <!-- <method name="Introspect"/> --> + </interface> + <interface name="com.redhat.idm.trust"> + <method name="fetch_domains"> + <helper exec="/usr/libexec/ipa/com.redhat.idm.trust-fetch-domains" + arguments="1" + argument_passing_method="cmdline" + prepend_user_name="no"/> + </method> + </interface> + </object> + </service> +</oddjobconfig> diff --git a/ipalib/plugins/trust.py b/ipalib/plugins/trust.py index 13ac52ddd..9fbaf2507 100644 --- a/ipalib/plugins/trust.py +++ b/ipalib/plugins/trust.py @@ -22,6 +22,7 @@ from ipalib.plugable import Registry from ipalib.plugins.baseldap import * from ipalib.plugins.dns import dns_container_exists from ipapython.ipautil import realm_to_suffix +from ipapython.ipa_log_manager import root_logger from ipalib import api, Str, StrEnum, Password, Bool, _, ngettext from ipalib import Command from ipalib import errors @@ -43,6 +44,8 @@ except Exception, e: if api.env.in_server and api.env.context in ['lite', 'server']: try: import ipaserver.dcerpc #pylint: disable=F0401 + from ipaserver.dcerpc import TRUST_ONEWAY, TRUST_BIDIRECTIONAL + import dbus, dbus.mainloop.glib _bindings_installed = True except ImportError: _bindings_installed = False @@ -161,6 +164,8 @@ _trust_type_option = StrEnum('trust_type', DEFAULT_RANGE_SIZE = 200000 +DBUS_IFACE_TRUST = 'com.redhat.idm.trust' + def trust_type_string(level): """ Returns a string representing a type of the trust. The original field is an enum: @@ -191,7 +196,7 @@ def make_trust_dn(env, trust_type, dn): return DN(dn, container_dn) return dn -def add_range(self, range_name, dom_sid, *keys, **options): +def add_range(myapi, 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. @@ -224,7 +229,7 @@ def add_range(self, range_name, dom_sid, *keys, **options): + basedn # Get the domain validator - domain_validator = ipaserver.dcerpc.DomainValidator(self.api) + domain_validator = ipaserver.dcerpc.DomainValidator(myapi) if not domain_validator.is_configured(): raise errors.NotFound( reason=_('Cannot search in trusted domains without own ' @@ -251,10 +256,10 @@ def add_range(self, range_name, dom_sid, *keys, **options): 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") + root_logger.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") + root_logger.debug("Able to gain POSIX info from the AD") range_type = u'ipa-ad-trust-posix' max_uid = info.get('msSFU30MaxUidNumber') @@ -288,16 +293,43 @@ def add_range(self, range_name, dom_sid, *keys, **options): ) * DEFAULT_RANGE_SIZE # Finally, add new ID range - self.api.Command['idrange_add'](range_name, - ipabaseid=base_id, - ipaidrangesize=range_size, - ipabaserid=0, - iparangetype=range_type, - ipanttrusteddomainsid=dom_sid) + myapi.Command['idrange_add'](range_name, + ipabaseid=base_id, + ipaidrangesize=range_size, + ipabaserid=0, + iparangetype=range_type, + ipanttrusteddomainsid=dom_sid) # Return the values that were generated inside this function return range_type, range_size, base_id +def fetch_trusted_domains_over_dbus(myapi, log, forest_name): + if not _bindings_installed: + return + # Calling oddjobd-activated service via DBus has some quirks: + # - Oddjobd registers multiple canonical names on the same address + # - python-dbus only follows name owner changes when mainloop is in use + # See https://fedorahosted.org/oddjob/ticket/2 for details + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + try: + _ret = 0 + _stdout = '' + _stderr = '' + bus = dbus.SystemBus() + intf = bus.get_object(DBUS_IFACE_TRUST,"/", follow_name_owner_changes=True) + fetch_domains_method = intf.get_dbus_method('fetch_domains', dbus_interface=DBUS_IFACE_TRUST) + (_ret, _stdout, _stderr) = fetch_domains_method(forest_name) + except dbus.DBusException, e: + log.error('Failed to call %(iface)s.fetch_domains helper.' + 'DBus exception is %(exc)s.' % dict(iface=DBUS_IFACE_TRUST, exc=str(e))) + if _ret != 0: + log.error('Helper was called for forest %(forest)s, return code is %(ret)d' % dict(forest=forest_name, ret=_ret)) + log.error('Standard output from the helper:\n%s---\n' % (_stdout)) + log.error('Error output from the helper:\n%s--\n' % (_stderr)) + raise errors.ServerCommandError(server=myapi.env.host, + error=_('Fetching domains from trusted forest failed. ' + 'See details in the error_log')) + return @register() class trust(LDAPObject): @@ -463,6 +495,12 @@ sides. .format(vals=', '.join(range_types.keys())))), values=tuple(range_types.keys()), ), + Bool('bidirectional?', + label=_('Two-way trust'), + cli_name='two_way', + doc=(_('Establish bi-directional trust. By default trust is inbound one-way only.')), + default=False, + ), ) msg_summary = _('Added Active Directory trust for realm "%(value)s"') @@ -478,7 +516,7 @@ sides. # Store the created range type, since for POSIX trusts no # ranges for the subdomains should be added, POSIX attributes # provide a global mapping across all subdomains - (created_range_type, _, _) = add_range(self, range_name, dom_sid, + (created_range_type, _, _) = add_range(self.api, range_name, dom_sid, *keys, **options) else: created_range_type = old_range['result']['iparangetype'][0] @@ -486,19 +524,35 @@ sides. trust_filter = "cn=%s" % result['value'] ldap = self.obj.backend (trusts, truncated) = ldap.find_entries( - base_dn=DN(api.env.container_trusts, api.env.basedn), + base_dn=DN(self.api.env.container_trusts, self.api.env.basedn), filter=trust_filter) result['result'] = entry_to_dict(trusts[0], **options) # Fetch topology of the trust forest -- we need always to do it # for AD trusts, regardless of the type of idranges associated with it - # Note that fetch_domains_from_trust will add needed ranges for + # Note that add_new_domains_from_trust will add needed ranges for # the algorithmic ID mapping case. if (options.get('trust_type') == u'ad' and options.get('trust_secret') is None): - domains = fetch_domains_from_trust(self, self.trustinstance, + if options.get('bidirectional') == True: + # Bidirectional trust allows us to use cross-realm TGT, so we can + # run the call under original user's credentials + res = fetch_domains_from_trust(self.api, self.trustinstance, result['result'], **options) + domains = add_new_domains_from_trust(self.api, self.trustinstance, + result['result'], res, **options) + else: + # One-way trust is more complex. We don't have cross-realm TGT + # and cannot use IPA principals to authenticate against AD. + # Instead, we have to use our trusted domain object's (TDO) + # account in AD. Access to the credentials is limited and IPA + # framework cannot access it directly. Instead, we call out to + # oddjobd-activated higher privilege process that will use TDO + # object credentials to authenticate to AD with Kerberos, + # run DCE RPC calls to do discovery and will call + # add_new_domains_from_trust() on its own. + fetch_trusted_domains_over_dbus(self.api, self.log, result['value']) # Format the output into human-readable values result['result']['trusttype'] = [trust_type_string( @@ -570,7 +624,7 @@ sides. # If domain name and realm does not match, IPA server is not be able # to establish trust with Active Directory. - realm_not_matching_domain = (api.env.domain.upper() != api.env.realm) + realm_not_matching_domain = (self.api.env.domain.upper() != self.api.env.realm) if options['trust_type'] == u'ad' and realm_not_matching_domain: raise errors.ValidationError( @@ -627,7 +681,7 @@ sides. range_type = options.get('range_type') try: - old_range = api.Command['idrange_show'](range_name, raw=True) + old_range = self.api.Command['idrange_show'](range_name, raw=True) except errors.NotFound: old_range = None @@ -699,6 +753,9 @@ sides. except errors.NotFound: dn = None + trust_type = TRUST_ONEWAY + if options.get('bidirectional', False): + trust_type = TRUST_BIDIRECTIONAL # 1. Full access to the remote domain. Use admin credentials and # generate random trustdom password to do work on both sides if full_join: @@ -707,14 +764,15 @@ sides. keys[-1], self.realm_server, self.realm_admin, - self.realm_passwd + self.realm_passwd, + trust_type ) except errors.NotFound: error_message=_("Unable to resolve domain controller for '%s' domain. ") % (keys[-1]) instructions=[] if dns_container_exists(self.obj.backend): try: - dns_zone = api.Command.dnszone_show(keys[-1])['result'] + dns_zone = self.api.Command.dnszone_show(keys[-1])['result'] if ('idnsforwardpolicy' in dns_zone) and dns_zone['idnsforwardpolicy'][0] == u'only': instructions.append(_("Forward policy is defined for it in IPA DNS, " "perhaps forwarder points to incorrect host?")) @@ -755,7 +813,8 @@ sides. result = self.trustinstance.join_ad_ipa_half( keys[-1], self.realm_server, - options['trust_secret'] + options['trust_secret'], + trust_type ) ret = dict( value=pkey_to_value( @@ -940,7 +999,7 @@ class trustconfig(LDAPObject): group, ['posixgroup'], [''], - DN(api.env.container_group, api.env.basedn)) + DN(self.api.env.container_group, self.api.env.basedn)) except errors.NotFound: self.api.Object['group'].handle_not_found(group) else: @@ -1066,11 +1125,11 @@ class adtrust_is_enabled(Command): ldap = self.api.Backend.ldap2 adtrust_dn = DN( ('cn', 'ADTRUST'), - ('cn', api.env.host), + ('cn', self.api.env.host), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), - api.env.basedn + self.api.env.basedn ) try: @@ -1281,7 +1340,7 @@ class trustdomain_del(LDAPDelete): raise errors.ValidationError(name='domain', error=_("cannot delete root domain of the trust, use trust-del to delete the trust itself")) try: - res = api.Command.trustdomain_enable(keys[0], domain) + res = self.api.Command.trustdomain_enable(keys[0], domain) except errors.AlreadyActive: pass result = super(trustdomain_del, self).execute(*keys, **options) @@ -1291,7 +1350,7 @@ class trustdomain_del(LDAPDelete): -def fetch_domains_from_trust(self, trustinstance, trust_entry, **options): +def fetch_domains_from_trust(myapi, trustinstance, trust_entry, **options): trust_name = trust_entry['cn'][0] creds = None password = options.get('realm_passwd', None) @@ -1303,16 +1362,20 @@ def fetch_domains_from_trust(self, trustinstance, trust_entry, **options): creds = u"{name}%{password}".format(name="\\".join(sp), password=password) server = options.get('realm_server', None) - domains = ipaserver.dcerpc.fetch_domains(self.api, + domains = ipaserver.dcerpc.fetch_domains(myapi, trustinstance.local_flatname, trust_name, creds=creds, server=server) + return domains + +def add_new_domains_from_trust(myapi, trustinstance, trust_entry, domains, **options): result = [] if not domains: return result - # trust range must exist by the time fetch_domains_from_trust is called + trust_name = trust_entry['cn'][0] + # trust range must exist by the time add_new_domains_from_trust is called range_name = trust_name.upper() + '_id_range' - old_range = api.Command.idrange_show(range_name, raw=True)['result'] + old_range = myapi.Command.idrange_show(range_name, raw=True)['result'] idrange_type = old_range['iparangetype'][0] for dom in domains: @@ -1325,13 +1388,13 @@ def fetch_domains_from_trust(self, trustinstance, trust_entry, **options): if 'raw' in options: dom['raw'] = options['raw'] - res = self.api.Command.trustdomain_add(trust_name, name, **dom) + res = myapi.Command.trustdomain_add(trust_name, name, **dom) result.append(res['result']) if idrange_type != u'ipa-ad-trust-posix': range_name = name.upper() + '_id_range' dom['range_type'] = u'ipa-ad-trust' - add_range(self, range_name, dom['ipanttrusteddomainsid'], + add_range(myapi, range_name, dom['ipanttrusteddomainsid'], trust_name, name, **dom) except errors.DuplicateEntry: # Ignore updating duplicate entries @@ -1362,6 +1425,17 @@ class trust_fetch_domains(LDAPRetrieve): ) trust = self.api.Command.trust_show(keys[0], raw=True)['result'] + result = dict() + result['result'] = [] + result['count'] = 0 + result['truncated'] = False + + # For one-way trust fetch over DBus. we don't get the list in this case. + if trust['ipanttrustdirection'] & TRUST_BIDIRECTIONAL != TRUST_BIDIRECTIONAL: + fetch_trusted_domains_over_dbus(self.api, self.log, keys[0]) + result['summary'] = unicode(_('List of trust domains successfully refreshed. Use trustdomain-find command to list them.')) + return result + trustinstance = ipaserver.dcerpc.TrustDomainJoins(self.api) if not trustinstance.configured: raise errors.NotFound( @@ -1372,8 +1446,8 @@ class trust_fetch_domains(LDAPRetrieve): 'on the IPA server first' ) ) - domains = fetch_domains_from_trust(self, trustinstance, trust) - result = dict() + res = fetch_domains_from_trust(self.api, trustinstance, trust, **options) + domains = add_new_domains_from_trust(self.api, trustinstance, trust, res, **options) if len(domains) > 0: result['summary'] = unicode(_('List of trust domains successfully refreshed')) @@ -1382,7 +1456,6 @@ class trust_fetch_domains(LDAPRetrieve): result['result'] = domains result['count'] = len(domains) - result['truncated'] = False return result @@ -1413,7 +1486,7 @@ class trustdomain_enable(LDAPQuery): trust_entry['ipantsidblacklistincoming'].remove(sid) ldap.update_entry(trust_entry) # Force MS-PAC cache re-initialization on KDC side - domval = ipaserver.dcerpc.DomainValidator(api) + domval = ipaserver.dcerpc.DomainValidator(self.api) (ccache_name, principal) = domval.kinit_as_http(keys[0]) else: raise errors.AlreadyActive() @@ -1453,7 +1526,7 @@ class trustdomain_disable(LDAPQuery): trust_entry['ipantsidblacklistincoming'].append(sid) ldap.update_entry(trust_entry) # Force MS-PAC cache re-initialization on KDC side - domval = ipaserver.dcerpc.DomainValidator(api) + domval = ipaserver.dcerpc.DomainValidator(self.api) (ccache_name, principal) = domval.kinit_as_http(keys[0]) else: raise errors.AlreadyInactive() diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py index 753e10e97..b11233d8d 100644 --- a/ipaserver/dcerpc.py +++ b/ipaserver/dcerpc.py @@ -66,6 +66,10 @@ The code in this module relies heavily on samba4-python package and Samba4 python bindings. """) +# Both constants can be used as masks against trust direction +# because bi-directional has two lower bits set. +TRUST_ONEWAY = 1 +TRUST_BIDIRECTIONAL = 3 def is_sid_valid(sid): try: @@ -949,7 +953,7 @@ class TrustDomainInstance(object): # We can ignore the error here -- setting up name suffix routes may fail pass - def establish_trust(self, another_domain, trustdom_secret): + def establish_trust(self, another_domain, trustdom_secret, trust_type='bidirectional'): """ Establishes trust between our and another domain Input: another_domain -- instance of TrustDomainInstance, initialized with #retrieve call @@ -967,7 +971,9 @@ class TrustDomainInstance(object): info.domain_name.string = another_domain.info['dns_domain'] info.netbios_name.string = another_domain.info['name'] info.sid = security.dom_sid(another_domain.info['sid']) - info.trust_direction = lsa.LSA_TRUST_DIRECTION_INBOUND | lsa.LSA_TRUST_DIRECTION_OUTBOUND + info.trust_direction = lsa.LSA_TRUST_DIRECTION_INBOUND + if trust_type == TRUST_BIDIRECTIONAL: + info.trust_direction |= lsa.LSA_TRUST_DIRECTION_OUTBOUND info.trust_type = lsa.LSA_TRUST_TYPE_UPLEVEL info.trust_attributes = 0 @@ -1005,7 +1011,8 @@ class TrustDomainInstance(object): pass try: - info.trust_attributes = lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE + info = self._pipe.QueryTrustedDomainInfo(trustdom_handle, lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX) + info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE self._pipe.SetInformationTrustedDomain(trustdom_handle, lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX, info) except RuntimeError, e: root_logger.error('unable to set trust to transitive: %s' % (str(e))) @@ -1014,10 +1021,10 @@ class TrustDomainInstance(object): self.update_ftinfo(another_domain) def verify_trust(self, another_domain): - def retrieve_netlogon_info_2(domain, function_code, data): + def retrieve_netlogon_info_2(logon_server, domain, function_code, data): try: netr_pipe = netlogon.netlogon(domain.binding, domain.parm, domain.creds) - result = netr_pipe.netr_LogonControl2Ex(logon_server=None, + result = netr_pipe.netr_LogonControl2Ex(logon_server=logon_server, function_code=function_code, level=2, data=data @@ -1026,7 +1033,7 @@ class TrustDomainInstance(object): except RuntimeError, (num, message): raise assess_dcerpc_exception(num=num, message=message) - result = retrieve_netlogon_info_2(self, + result = retrieve_netlogon_info_2(None, self, netlogon.NETLOGON_CONTROL_TC_VERIFY, another_domain.info['dns_domain']) if (result and (result.flags and netlogon.NETLOGON_VERIFY_STATUS_RETURNED)): @@ -1098,6 +1105,7 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None): td.info['dc'] = unicode(result.pdc_dns_name) if creds is None: + # Attempt to authenticate as HTTP/ipa.master and use cross-forest trust domval = DomainValidator(api) (ccache_name, principal) = domval.kinit_as_http(trustdomain) td.creds = credentials.Credentials() @@ -1107,7 +1115,15 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None): td.creds.guess(td.parm) td.creds.set_workstation(domain_validator.flatname) domains = communicate(td) + elif type(creds) is bool: + # Rely on existing Kerberos credentials in the environment + td.creds = credentials.Credentials() + td.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS) + td.creds.guess(td.parm) + td.creds.set_workstation(domain_validator.flatname) + domains = communicate(td) else: + # Assume we've got credentials as a string user%password td.creds = credentials.Credentials() td.creds.set_kerberos_state(credentials.DONT_USE_KERBEROS) td.creds.guess(td.parm) @@ -1220,7 +1236,7 @@ class TrustDomainJoins(object): ftinfo['rec_type'] = lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME self.local_domain.ftinfo_records.append(ftinfo) - def join_ad_full_credentials(self, realm, realm_server, realm_admin, realm_passwd): + def join_ad_full_credentials(self, realm, realm_server, realm_admin, realm_passwd, trust_type): if not self.configured: return None @@ -1238,13 +1254,17 @@ class TrustDomainJoins(object): if not self.remote_domain.read_only: trustdom_pass = samba.generate_random_password(128, 128) self.get_realmdomains() - self.remote_domain.establish_trust(self.local_domain, trustdom_pass) - self.local_domain.establish_trust(self.remote_domain, trustdom_pass) - result = self.remote_domain.verify_trust(self.local_domain) + self.remote_domain.establish_trust(self.local_domain, trustdom_pass, trust_type) + self.local_domain.establish_trust(self.remote_domain, trustdom_pass, trust_type) + # if trust is inbound, we don't need to verify it because AD DC will respond + # with WERR_NO_SUCH_DOMAIN -- in only does verification for outbound trusts. + result = True + if trust_type == TRUST_BIDIRECTIONAL: + result = self.remote_domain.verify_trust(self.local_domain) return dict(local=self.local_domain, remote=self.remote_domain, verified=result) return None - def join_ad_ipa_half(self, realm, realm_server, trustdom_passwd): + def join_ad_ipa_half(self, realm, realm_server, trustdom_passwd, trust_type): if not self.configured: return None @@ -1254,5 +1274,5 @@ class TrustDomainJoins(object): if self.remote_domain.info['dns_domain'] != self.remote_domain.info['dns_forest']: raise errors.NotAForestRootError(forest=self.remote_domain.info['dns_forest'], domain=self.remote_domain.info['dns_domain']) - self.local_domain.establish_trust(self.remote_domain, trustdom_passwd) + self.local_domain.establish_trust(self.remote_domain, trustdom_passwd, trust_type) return dict(local=self.local_domain, remote=self.remote_domain, verified=False) |