86baa9
From a9ff7e93de94de1c7ecaedce582d598d6a4ad30e Mon Sep 17 00:00:00 2001
86baa9
From: Alexander Bokovoy <abokovoy@redhat.com>
86baa9
Date: Thu, 27 Jun 2019 15:17:07 +0300
86baa9
Subject: [PATCH] trust-fetch-domains: make sure we use right KDC when --server
86baa9
 is specified
86baa9
86baa9
Since we are authenticating against AD DC before talking to it (by using
86baa9
trusted domain object's credentials), we need to override krb5.conf
86baa9
configuration in case --server option is specified.
86baa9
86baa9
The context is a helper which is launched out of process with the help
86baa9
of oddjobd. The helper takes existing trusted domain object, uses its
86baa9
credentials to authenticate and then runs LSA RPC calls against that
86baa9
trusted domain's domain controller. Previous code directed Samba
86baa9
bindings to use the correct domain controller. However, if a DC visible
86baa9
to MIT Kerberos is not reachable, we would not be able to obtain TGT and
86baa9
the whole process will fail.
86baa9
86baa9
trust_add.execute() was calling out to the D-Bus helper without passing
86baa9
the options (e.g. --server) so there was no chance to get that option
86baa9
visible by the oddjob helper.
86baa9
86baa9
Also we need to make errors in the oddjob helper more visible to
86baa9
error_log. Thus, move error reporting for a normal communication up from
86baa9
the exception catching.
86baa9
86baa9
Resolves: https://pagure.io/freeipa/issue/7895
86baa9
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
86baa9
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
86baa9
---
86baa9
 .../oddjob/com.redhat.idm.trust-fetch-domains | 233 +++++++++++++-----
86baa9
 ipaserver/plugins/trust.py                    |  28 ++-
86baa9
 2 files changed, 194 insertions(+), 67 deletions(-)
86baa9
86baa9
diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains b/install/oddjob/com.redhat.idm.trust-fetch-domains
86baa9
index 029de781b2a1f94b1fd281b79aa69a1e9422dede..b406201ec2baf8ee9398fc3634060b6d9a5392b0 100755
86baa9
--- a/install/oddjob/com.redhat.idm.trust-fetch-domains
86baa9
+++ b/install/oddjob/com.redhat.idm.trust-fetch-domains
86baa9
@@ -8,9 +8,12 @@ from ipapython.dn import DN
86baa9
 from ipapython.dnsutil import DNSName
86baa9
 from ipaplatform.constants import constants
86baa9
 from ipaplatform.paths import paths
86baa9
+import io
86baa9
 import sys
86baa9
 import os
86baa9
 import pwd
86baa9
+import tempfile
86baa9
+import textwrap
86baa9
 
86baa9
 import six
86baa9
 import gssapi
86baa9
@@ -23,17 +26,38 @@ if six.PY3:
86baa9
 
86baa9
 def parse_options():
86baa9
     usage = "%prog <trusted domain name>\n"
86baa9
-    parser = config.IPAOptionParser(usage=usage,
86baa9
-                                    formatter=config.IPAFormatter())
86baa9
-
86baa9
-    parser.add_option("-d", "--debug", action="store_true", dest="debug",
86baa9
-                      help="Display debugging information")
86baa9
-    parser.add_option("-s", "--server", action="store", dest="server",
86baa9
-                      help="Domain controller for the Active Directory domain (optional)")
86baa9
-    parser.add_option("-a", "--admin", action="store", dest="admin",
86baa9
-                      help="Active Directory administrator (optional)")
86baa9
-    parser.add_option("-p", "--password", action="store", dest="password",
86baa9
-                      help="Display debugging information")
86baa9
+    parser = config.IPAOptionParser(
86baa9
+        usage=usage, formatter=config.IPAFormatter()
86baa9
+    )
86baa9
+
86baa9
+    parser.add_option(
86baa9
+        "-d",
86baa9
+        "--debug",
86baa9
+        action="store_true",
86baa9
+        dest="debug",
86baa9
+        help="Display debugging information",
86baa9
+    )
86baa9
+    parser.add_option(
86baa9
+        "-s",
86baa9
+        "--server",
86baa9
+        action="store",
86baa9
+        dest="server",
86baa9
+        help="Domain controller for the Active Directory domain (optional)",
86baa9
+    )
86baa9
+    parser.add_option(
86baa9
+        "-a",
86baa9
+        "--admin",
86baa9
+        action="store",
86baa9
+        dest="admin",
86baa9
+        help="Active Directory administrator (optional)",
86baa9
+    )
86baa9
+    parser.add_option(
86baa9
+        "-p",
86baa9
+        "--password",
86baa9
+        action="store",
86baa9
+        dest="password",
86baa9
+        help="Display debugging information",
86baa9
+    )
86baa9
 
86baa9
     options, args = parser.parse_args()
86baa9
     safe_options = parser.get_safe_opts(options)
86baa9
@@ -50,17 +74,26 @@ def parse_options():
86baa9
         raise ScriptError("You must specify a valid trusted domain name", 2)
86baa9
     return safe_options, options, trusted_domain
86baa9
 
86baa9
+
86baa9
 def retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal):
86baa9
-    getkeytab_args = ["/usr/sbin/ipa-getkeytab",
86baa9
-                      "-s", api.env.host,
86baa9
-                      "-p", oneway_principal,
86baa9
-                      "-k", oneway_keytab_name,
86baa9
-                      "-r"]
86baa9
+    getkeytab_args = [
86baa9
+        "/usr/sbin/ipa-getkeytab",
86baa9
+        "-s",
86baa9
+        api.env.host,
86baa9
+        "-p",
86baa9
+        oneway_principal,
86baa9
+        "-k",
86baa9
+        oneway_keytab_name,
86baa9
+        "-r",
86baa9
+    ]
86baa9
     if os.path.isfile(oneway_keytab_name):
86baa9
         os.unlink(oneway_keytab_name)
86baa9
 
86baa9
-    ipautil.run(getkeytab_args, env={'KRB5CCNAME': ccache_name, 'LANG': 'C'},
86baa9
-                raiseonerr=False)
86baa9
+    ipautil.run(
86baa9
+        getkeytab_args,
86baa9
+        env={"KRB5CCNAME": ccache_name, "LANG": "C"},
86baa9
+        raiseonerr=False,
86baa9
+    )
86baa9
     # Make sure SSSD is able to read the keytab
86baa9
     try:
86baa9
         sssd = pwd.getpwnam(constants.SSSD_USER)
86baa9
@@ -72,8 +105,7 @@ def retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal):
86baa9
 
86baa9
 
86baa9
 def get_forest_root_domain(api_instance, trusted_domain, server=None):
86baa9
-    """
86baa9
-    retrieve trusted forest root domain for given domain name
86baa9
+    """Retrieve trusted forest root domain for given domain name
86baa9
 
86baa9
     :param api_instance: IPA API instance
86baa9
     :param trusted_domain: trusted domain name
86baa9
@@ -81,18 +113,51 @@ def get_forest_root_domain(api_instance, trusted_domain, server=None):
86baa9
     :returns: forest root domain DNS name
86baa9
     """
86baa9
     trustconfig_show = api_instance.Command.trustconfig_show
86baa9
-    flatname = trustconfig_show()['result']['ipantflatname'][0]
86baa9
+    flatname = trustconfig_show()["result"]["ipantflatname"][0]
86baa9
 
86baa9
     remote_domain = dcerpc.retrieve_remote_domain(
86baa9
-        api_instance.env.host, flatname, trusted_domain,
86baa9
-        realm_server=server)
86baa9
+        api_instance.env.host, flatname, trusted_domain, realm_server=server
86baa9
+    )
86baa9
+
86baa9
+    return remote_domain.info["dns_forest"]
86baa9
+
86baa9
+
86baa9
+def generate_krb5_config(realm, server):
86baa9
+    """Generate override krb5 config file for trusted domain DC access
86baa9
+
86baa9
+    :param realm: realm of the trusted AD domain
86baa9
+    :param server: server to override KDC to
86baa9
+
86baa9
+    :returns: tuple (temporary config file name, KRB5_CONFIG string)
86baa9
+    """
86baa9
+    cfg = paths.KRB5_CONF
86baa9
+    tcfg = None
86baa9
+    if server:
86baa9
+        content = textwrap.dedent(u"""
86baa9
+            [realms]
86baa9
+               %s = {
86baa9
+                   kdc = %s
86baa9
+               }
86baa9
+            """) % (
86baa9
+            realm.upper(),
86baa9
+            server,
86baa9
+        )
86baa9
+
86baa9
+        (fd, tcfg) = tempfile.mkstemp(dir="/var/run/ipa",
86baa9
+                prefix="krb5conf", text=True)
86baa9
+        with io.open(fd, mode='w', encoding='utf-8') as o:
86baa9
+            o.write(content)
86baa9
+        cfg = ":".join([tcfg, cfg])
86baa9
+    return (tcfg, cfg)
86baa9
 
86baa9
-    return remote_domain.info['dns_forest']
86baa9
 
86baa9
 if not is_ipa_configured():
86baa9
     # LSB status code 6: program is not configured
86baa9
-    raise ScriptError("IPA is not configured " +
86baa9
-                      "(see man pages of ipa-server-install for help)", 6)
86baa9
+    raise ScriptError(
86baa9
+        "IPA is not configured "
86baa9
+        + "(see man pages of ipa-server-install for help)",
86baa9
+        6,
86baa9
+    )
86baa9
 
86baa9
 if not os.getegid() == 0:
86baa9
     # LSB status code 4: user had insufficient privilege
86baa9
@@ -100,8 +165,9 @@ if not os.getegid() == 0:
86baa9
 
86baa9
 safe_options, options, trusted_domain = parse_options()
86baa9
 
86baa9
-api.bootstrap(in_server=True, log=None,
86baa9
-              context='server', confdir=paths.ETC_IPA)
86baa9
+api.bootstrap(
86baa9
+    in_server=True, log=None, context="server", confdir=paths.ETC_IPA
86baa9
+)
86baa9
 api.finalize()
86baa9
 
86baa9
 # Only import trust plugin after api is initialized or internal imports
86baa9
@@ -121,12 +187,12 @@ from ipaserver.plugins import trust
86baa9
 # and retrieve our own NetBIOS domain name and use cifs/ipa.master@IPA.REALM to
86baa9
 # retrieve the keys to oneway_keytab_name.
86baa9
 
86baa9
-keytab_name = '/etc/samba/samba.keytab'
86baa9
+keytab_name = "/etc/samba/samba.keytab"
86baa9
 
86baa9
-principal = str('cifs/' + api.env.host)
86baa9
+principal = str("cifs/" + api.env.host)
86baa9
 
86baa9
-oneway_ccache_name = '/var/run/ipa/krb5cc_oddjob_trusts_fetch'
86baa9
-ccache_name = '/var/run/ipa/krb5cc_oddjob_trusts'
86baa9
+oneway_ccache_name = "/var/run/ipa/krb5cc_oddjob_trusts_fetch"
86baa9
+ccache_name = "/var/run/ipa/krb5cc_oddjob_trusts"
86baa9
 
86baa9
 # Standard sequence:
86baa9
 # - check if ccache exists
86baa9
@@ -140,33 +206,45 @@ try:
86baa9
     cred = kinit_keytab(principal, keytab_name, ccache_name)
86baa9
     if cred.lifetime > 0:
86baa9
         have_ccache = True
86baa9
-except gssapi.exceptions.ExpiredCredentialsError:
86baa9
+except (gssapi.exceptions.ExpiredCredentialsError, gssapi.raw.misc.GSSError):
86baa9
     pass
86baa9
 if not have_ccache:
86baa9
     # delete stale ccache and try again
86baa9
-    if os.path.exists(oneway_ccache_name):
86baa9
+    if os.path.exists(ccache_name):
86baa9
         os.unlink(ccache_name)
86baa9
     cred = kinit_keytab(principal, keytab_name, ccache_name)
86baa9
 
86baa9
-old_ccache = os.environ.get('KRB5CCNAME')
86baa9
+old_ccache = os.environ.get("KRB5CCNAME")
86baa9
+old_config = os.environ.get("KRB5_CONFIG")
86baa9
 api.Backend.ldap2.connect(ccache_name)
86baa9
 
86baa9
 # Retrieve own NetBIOS name and trusted forest's name.
86baa9
 # We use script's input to retrieve the trusted forest's name to sanitize input
86baa9
 # for file-level access as we might need to wipe out keytab in /var/lib/sss/keytabs
86baa9
-own_trust_dn = DN(('cn', api.env.domain),('cn','ad'), ('cn', 'etc'), api.env.basedn)
86baa9
-own_trust_entry = api.Backend.ldap2.get_entry(own_trust_dn, ['ipantflatname'])
86baa9
-own_trust_flatname = own_trust_entry.single_value.get('ipantflatname').upper()
86baa9
-trusted_domain_dn = DN(('cn', trusted_domain.lower()), api.env.container_adtrusts, api.env.basedn)
86baa9
-trusted_domain_entry = api.Backend.ldap2.get_entry(trusted_domain_dn, ['cn'])
86baa9
-trusted_domain = trusted_domain_entry.single_value.get('cn').lower()
86baa9
+own_trust_dn = DN(
86baa9
+    ("cn", api.env.domain), ("cn", "ad"), ("cn", "etc"), api.env.basedn
86baa9
+)
86baa9
+own_trust_entry = api.Backend.ldap2.get_entry(own_trust_dn, ["ipantflatname"])
86baa9
+own_trust_flatname = own_trust_entry.single_value.get("ipantflatname").upper()
86baa9
+trusted_domain_dn = DN(
86baa9
+    ("cn", trusted_domain.lower()), api.env.container_adtrusts, api.env.basedn
86baa9
+)
86baa9
+trusted_domain_entry = api.Backend.ldap2.get_entry(trusted_domain_dn, ["cn"])
86baa9
+trusted_domain = trusted_domain_entry.single_value.get("cn").lower()
86baa9
 
86baa9
 # At this point if we didn't find trusted forest name, an exception will be raised
86baa9
 # and script will quit. This is actually intended.
86baa9
 
86baa9
+# Generate MIT Kerberos configuration file that potentially overlays
86baa9
+# the KDC to connect to for a trusted domain to allow --server option
86baa9
+# to take precedence.
86baa9
+cfg_file, cfg = generate_krb5_config(trusted_domain, options.server)
86baa9
+
86baa9
 if not (options.admin and options.password):
86baa9
-    oneway_keytab_name = '/var/lib/sss/keytabs/' + trusted_domain + '.keytab'
86baa9
-    oneway_principal = str('%s$@%s' % (own_trust_flatname, trusted_domain.upper()))
86baa9
+    oneway_keytab_name = "/var/lib/sss/keytabs/" + trusted_domain + ".keytab"
86baa9
+    oneway_principal = str(
86baa9
+        "%s$@%s" % (own_trust_flatname, trusted_domain.upper())
86baa9
+    )
86baa9
 
86baa9
     # If keytab does not exist, retrieve it
86baa9
     if not os.path.isfile(oneway_keytab_name):
86baa9
@@ -176,39 +254,78 @@ if not (options.admin and options.password):
86baa9
         have_ccache = False
86baa9
         try:
86baa9
             # The keytab may have stale key material (from older trust-add run)
86baa9
-            cred = kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name)
86baa9
+            cred = kinit_keytab(
86baa9
+                oneway_principal,
86baa9
+                oneway_keytab_name,
86baa9
+                oneway_ccache_name,
86baa9
+                config=cfg,
86baa9
+            )
86baa9
             if cred.lifetime > 0:
86baa9
                 have_ccache = True
86baa9
-        except gssapi.exceptions.ExpiredCredentialsError:
86baa9
+        except (gssapi.exceptions.ExpiredCredentialsError, gssapi.raw.misc.GSSError):
86baa9
             pass
86baa9
         if not have_ccache:
86baa9
             if os.path.exists(oneway_ccache_name):
86baa9
                 os.unlink(oneway_ccache_name)
86baa9
-            kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name)
86baa9
-    except gssapi.exceptions.GSSError:
86baa9
+            kinit_keytab(
86baa9
+                oneway_principal,
86baa9
+                oneway_keytab_name,
86baa9
+                oneway_ccache_name,
86baa9
+                config=cfg,
86baa9
+            )
86baa9
+    except (gssapi.exceptions.GSSError, gssapi.raw.misc.GSSError):
86baa9
         # If there was failure on using keytab, assume it is stale and retrieve again
86baa9
         retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
86baa9
         if os.path.exists(oneway_ccache_name):
86baa9
             os.unlink(oneway_ccache_name)
86baa9
-        kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name)
86baa9
+        cred = kinit_keytab(
86baa9
+            oneway_principal,
86baa9
+            oneway_keytab_name,
86baa9
+            oneway_ccache_name,
86baa9
+            config=cfg,
86baa9
+        )
86baa9
 else:
86baa9
-    cred = kinit_password(options.admin, options.password,
86baa9
-                          oneway_ccache_name,
86baa9
-                          canonicalize=True, enterprise=True)
86baa9
+    cred = kinit_password(
86baa9
+        options.admin,
86baa9
+        options.password,
86baa9
+        oneway_ccache_name,
86baa9
+        canonicalize=True,
86baa9
+        enterprise=True,
86baa9
+        config=cfg,
86baa9
+    )
86baa9
+
86baa9
+if cred and cred.lifetime > 0:
86baa9
+    have_ccache = True
86baa9
+
86baa9
+if not have_ccache:
86baa9
+    sys.exit(1)
86baa9
 
86baa9
 # We are done: we have ccache with TDO credentials and can fetch domains
86baa9
 ipa_domain = api.env.domain
86baa9
-os.environ['KRB5CCNAME'] = oneway_ccache_name
86baa9
+os.environ["KRB5CCNAME"] = oneway_ccache_name
86baa9
+os.environ["KRB5_CONFIG"] = cfg
86baa9
 
86baa9
 # retrieve the forest root domain name and contact it to retrieve trust
86baa9
 # topology info
86baa9
-forest_root = get_forest_root_domain(api, trusted_domain, server=options.server)
86baa9
-
86baa9
-domains = dcerpc.fetch_domains(api, ipa_domain, forest_root, creds=True, server=options.server)
86baa9
-trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)['result']
86baa9
-trust.add_new_domains_from_trust(api, None, trust_domain_object, domains)
86baa9
+forest_root = get_forest_root_domain(
86baa9
+    api, trusted_domain, server=options.server
86baa9
+)
86baa9
+domains = dcerpc.fetch_domains(
86baa9
+    api, ipa_domain, forest_root, creds=True, server=options.server
86baa9
+)
86baa9
 
86baa9
 if old_ccache:
86baa9
-   os.environ['KRB5CCNAME'] = old_ccache
86baa9
+    os.environ["KRB5CCNAME"] = old_ccache
86baa9
+
86baa9
+if old_config:
86baa9
+    os.environ["KRB5_CONFIG"] = old_config
86baa9
+
86baa9
+if cfg_file:
86baa9
+    os.remove(cfg_file)
86baa9
+
86baa9
+trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)[
86baa9
+    "result"
86baa9
+]
86baa9
+trust.add_new_domains_from_trust(api, None, trust_domain_object, domains)
86baa9
 
86baa9
 sys.exit(0)
86baa9
diff --git a/ipaserver/plugins/trust.py b/ipaserver/plugins/trust.py
86baa9
index 5de363bda6fdee081d3e4c98e731cecfa9585d21..676b5f645a99c40a0c944096d7c6252a4c56f983 100644
86baa9
--- a/ipaserver/plugins/trust.py
86baa9
+++ b/ipaserver/plugins/trust.py
86baa9
@@ -424,11 +424,11 @@ def fetch_trusted_domains_over_dbus(myapi, *keys, **options):
86baa9
 
86baa9
     forest_name = keys[0]
86baa9
     method_options = []
86baa9
-    if 'realm_server' in options:
86baa9
+    if options.get('realm_server', None):
86baa9
         method_options.extend(['--server', options['realm_server']])
86baa9
-    if 'realm_admin' in options:
86baa9
+    if options.get('realm_admin', None):
86baa9
         method_options.extend(['--admin', options['realm_admin']])
86baa9
-    if 'realm_passwd' in options:
86baa9
+    if options.get('realm_passwd', None):
86baa9
         method_options.extend(['--password', options['realm_passwd']])
86baa9
 
86baa9
     # Calling oddjobd-activated service via DBus has some quirks:
86baa9
@@ -458,11 +458,15 @@ def fetch_trusted_domains_over_dbus(myapi, *keys, **options):
86baa9
     except dbus.DBusException as e:
86baa9
         logger.error('Failed to call %s.fetch_domains helper.'
86baa9
                      'DBus exception is %s.', DBUS_IFACE_TRUST, str(e))
86baa9
-        if _ret != 0:
86baa9
-            logger.error('Helper was called for forest %s, return code is %d',
86baa9
-                         forest_name, _ret)
86baa9
-            logger.error('Standard output from the helper:\n%s---\n', _stdout)
86baa9
-            logger.error('Error output from the helper:\n%s--\n', _stderr)
86baa9
+        _ret = 2
86baa9
+        _stdout = '<not available>'
86baa9
+        _stderr = '<not available>'
86baa9
+
86baa9
+    if _ret != 0:
86baa9
+        logger.error('Helper fetch_domains was called for forest %s, '
86baa9
+                     'return code is %d', forest_name, _ret)
86baa9
+        logger.error('Standard output from the helper:\n%s---\n', _stdout)
86baa9
+        logger.error('Error output from the helper:\n%s--\n', _stderr)
86baa9
         raise errors.ServerCommandError(
86baa9
             server=myapi.env.host,
86baa9
             error=_('Fetching domains from trusted forest failed. '
86baa9
@@ -801,7 +805,13 @@ ipa idrange-del before retrying the command with the desired range type.
86baa9
                 # object credentials to authenticate to AD with Kerberos,
86baa9
                 # run DCE RPC calls to do discovery and will call
86baa9
                 # add_new_domains_from_trust() on its own.
86baa9
-                fetch_trusted_domains_over_dbus(self.api, result['value'])
86baa9
+                # We only pass through the realm_server option because we need
86baa9
+                # to reach the specified Active Directory domain controller
86baa9
+                # No need to pass through admin credentials as we have TDO
86baa9
+                # credentials at this point already
86baa9
+                fetch_trusted_domains_over_dbus(self.api, result['value'],
86baa9
+                                                realm_server=options.get(
86baa9
+                                                    'realm_server', None))
86baa9
 
86baa9
         # Format the output into human-readable values unless `--raw` is given
86baa9
         self._format_trust_attrs(result, **options)
86baa9
-- 
86baa9
2.20.1
86baa9