summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander Bokovoy <abokovoy@redhat.com>2015-06-05 12:57:02 +0000
committerAlexander Bokovoy <abokovoy@redhat.com>2015-07-07 11:09:03 +0300
commit3d975c104be2bd68df53617bc82883aa1a001833 (patch)
tree66023bd0ad5aa4c413764688cc392dabe9afad6b
parent65422777e6721bcf7708805f001eb39016495830 (diff)
downloadfreeipa-3d975c104be2bd68df53617bc82883aa1a001833.tar.gz
freeipa-3d975c104be2bd68df53617bc82883aa1a001833.tar.xz
freeipa-3d975c104be2bd68df53617bc82883aa1a001833.zip
trusts: add support for one-way trust and switch to it by default
One-way trust is the default now, use 'trust add --two-way ' to force bidirectional trust https://fedorahosted.org/freeipa/ticket/4959 In case of one-way trust we cannot authenticate using cross-realm TGT against an AD DC. We have to use trusted domain object from within AD domain and access to this object is limited to avoid compromising the whole trust configuration. Instead, IPA framework can call out to oddjob daemon and ask it to run the script which can have access to the TDO object. This script (com.redhat.idm.trust-fetch-domains) is using cifs/ipa.master principal to retrieve TDO object credentials from IPA LDAP if needed and then authenticate against AD DCs using the TDO object credentials. The script pulls the trust topology out of AD DCs and updates IPA LDAP store. Then IPA framework can pick the updated data from the IPA LDAP under normal access conditions. Part of https://fedorahosted.org/freeipa/ticket/4546
-rw-r--r--API.txt3
-rw-r--r--VERSION2
-rw-r--r--freeipa.spec.in14
-rw-r--r--install/Makefile.am1
-rw-r--r--install/configure.ac1
-rw-r--r--install/oddjob/Makefile.am28
-rwxr-xr-xinstall/oddjob/com.redhat.idm.trust-fetch-domains198
-rw-r--r--install/oddjob/etc/dbus-1/system.d/oddjob-ipa-trust.conf40
-rw-r--r--install/oddjob/etc/oddjobd.conf.d/oddjobd-ipa-trust.conf21
-rw-r--r--ipalib/plugins/trust.py141
-rw-r--r--ipaserver/dcerpc.py44
11 files changed, 442 insertions, 51 deletions
diff --git a/API.txt b/API.txt
index f3b4df8d4..020639f02 100644
--- a/API.txt
+++ b/API.txt
@@ -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')
diff --git a/VERSION b/VERSION
index c31ddfc97..02ad7e23c 100644
--- a/VERSION
+++ b/VERSION
@@ -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)