Blame SOURCES/0022-rpcserver-fallback-to-non-armored-kinit-in-case-of-trusted-domains_rhbz#1914821.patch

cce5df
From 1441b999d3fe9b4e59fe942294d13480ecee7d94 Mon Sep 17 00:00:00 2001
cce5df
From: Alexander Bokovoy <abokovoy@redhat.com>
cce5df
Date: Wed, 28 Oct 2020 17:46:56 +0200
cce5df
Subject: [PATCH] rpcserver: fallback to non-armored kinit in case of trusted
cce5df
 domains
cce5df
cce5df
MIT Kerberos implements FAST negotiation as specified in RFC 6806
cce5df
section 11. The implementation relies on the caller to provide a hint
cce5df
whether FAST armoring must be used.
cce5df
cce5df
FAST armor can only be used when both client and KDC have a shared
cce5df
secret. When KDC is from a trusted domain, there is no way to have a
cce5df
shared secret between a generic Kerberos client and that KDC.
cce5df
cce5df
[MS-KILE] section 3.2.5.4 'Using FAST When the Realm Supports FAST'
cce5df
allows KILE clients (Kerberos clients) to have local settings that
cce5df
direct it to enforce use of FAST. This is equal to the current
cce5df
implementation of 'kinit' utility in MIT Kerberos requiring to use FAST
cce5df
if armor cache (option '-T') is provided.
cce5df
cce5df
[MS-KILE] section 3.3.5.7.4 defines a way for a computer from a
cce5df
different realm to use compound identity TGS-REQ to create FAST TGS-REQ
cce5df
explicitly armored with the computer's TGT. However, this method is not
cce5df
available to IPA framework as we don't have access to the IPA server's
cce5df
host key. In addition, 'kinit' utility does not support this method.
cce5df
cce5df
Active Directory has a policy to force use of FAST when client
cce5df
advertizes its use. Since we cannot know in advance whether a principal
cce5df
to obtain initial credentials for belongs to our realm or to a trusted
cce5df
one due to enterprise principal canonicalization, we have to try to
cce5df
kinit. Right now we fail unconditionally if FAST couldn't be used and
cce5df
libkrb5 communication with a KDC from the user realm (e.g. from a
cce5df
trusted forest) causes enforcement of a FAST.
cce5df
cce5df
In the latter case, as we cannot use FAST anyway, try to kinit again
cce5df
without advertizing FAST. This works even in the situations when FAST
cce5df
enforcement is enabled on Active Directory side: if client doesn't
cce5df
advertize FAST capability, it is not required. Additionally, FAST cannot
cce5df
be used for any practical need for a trusted domain's users yet.
cce5df
cce5df
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
cce5df
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
cce5df
---
cce5df
 ipalib/errors.py                        |  6 ++
cce5df
 ipaserver/rpcserver.py                  | 94 ++++++++++++++++---------
cce5df
 ipatests/test_integration/test_trust.py | 21 ++++++
cce5df
 3 files changed, 86 insertions(+), 35 deletions(-)
cce5df
cce5df
diff --git a/ipalib/errors.py b/ipalib/errors.py
cce5df
index 1b17ca7ed..fa51e15c0 100644
cce5df
--- a/ipalib/errors.py
cce5df
+++ b/ipalib/errors.py
cce5df
@@ -245,6 +245,12 @@ class PluginModuleError(PrivateError):
cce5df
     format = '%(name)s is not a valid plugin module'
cce5df
 
cce5df
 
cce5df
+class KrbPrincipalWrongFAST(PrivateError):
cce5df
+    """
cce5df
+    Raised when it is not possible to use our FAST armor for kinit
cce5df
+    """
cce5df
+    format = '%(principal)s cannot use Anonymous PKINIT as a FAST armor'
cce5df
+
cce5df
 ##############################################################################
cce5df
 # Public errors:
cce5df
 
cce5df
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
cce5df
index 181295471..ed775170e 100644
cce5df
--- a/ipaserver/rpcserver.py
cce5df
+++ b/ipaserver/rpcserver.py
cce5df
@@ -46,9 +46,11 @@ from ipalib.capabilities import VERSION_WITHOUT_CAPABILITIES
cce5df
 from ipalib.frontend import Local
cce5df
 from ipalib.install.kinit import kinit_armor, kinit_password
cce5df
 from ipalib.backend import Executioner
cce5df
-from ipalib.errors import (PublicError, InternalError, JSONError,
cce5df
+from ipalib.errors import (
cce5df
+    PublicError, InternalError, JSONError,
cce5df
     CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError,
cce5df
-    ExecutionError, PasswordExpired, KrbPrincipalExpired, UserLocked)
cce5df
+    ExecutionError, PasswordExpired, KrbPrincipalExpired, KrbPrincipalWrongFAST,
cce5df
+    UserLocked)
cce5df
 from ipalib.request import context, destroy_context
cce5df
 from ipalib.rpc import (xml_dumps, xml_loads,
cce5df
     json_encode_binary, json_decode_binary)
cce5df
@@ -957,6 +959,34 @@ class login_password(Backend, KerberosSession):
cce5df
         self.api.Backend.wsgi_dispatch.mount(self, self.key)
cce5df
 
cce5df
     def __call__(self, environ, start_response):
cce5df
+        def attempt_kinit(user_principal, password,
cce5df
+                          ipa_ccache_name, use_armor=True):
cce5df
+            try:
cce5df
+                # try to remove in case an old file was there
cce5df
+                os.unlink(ipa_ccache_name)
cce5df
+            except OSError:
cce5df
+                pass
cce5df
+            try:
cce5df
+                self.kinit(user_principal, password,
cce5df
+                           ipa_ccache_name, use_armor=use_armor)
cce5df
+            except PasswordExpired as e:
cce5df
+                return self.unauthorized(environ, start_response,
cce5df
+                                         str(e), 'password-expired')
cce5df
+            except InvalidSessionPassword as e:
cce5df
+                return self.unauthorized(environ, start_response,
cce5df
+                                         str(e), 'invalid-password')
cce5df
+            except KrbPrincipalExpired as e:
cce5df
+                return self.unauthorized(environ,
cce5df
+                                         start_response,
cce5df
+                                         str(e),
cce5df
+                                         'krbprincipal-expired')
cce5df
+            except UserLocked as e:
cce5df
+                return self.unauthorized(environ,
cce5df
+                                         start_response,
cce5df
+                                         str(e),
cce5df
+                                         'user-locked')
cce5df
+            return None
cce5df
+
cce5df
         logger.debug('WSGI login_password.__call__:')
cce5df
 
cce5df
         # Get the user and password parameters from the request
cce5df
@@ -1007,26 +1037,14 @@ class login_password(Backend, KerberosSession):
cce5df
         ipa_ccache_name = os.path.join(paths.IPA_CCACHES,
cce5df
                                        'kinit_{}'.format(os.getpid()))
cce5df
         try:
cce5df
-            # try to remove in case an old file was there
cce5df
-            os.unlink(ipa_ccache_name)
cce5df
-        except OSError:
cce5df
-            pass
cce5df
-        try:
cce5df
-            self.kinit(user_principal, password, ipa_ccache_name)
cce5df
-        except PasswordExpired as e:
cce5df
-            return self.unauthorized(environ, start_response, str(e), 'password-expired')
cce5df
-        except InvalidSessionPassword as e:
cce5df
-            return self.unauthorized(environ, start_response, str(e), 'invalid-password')
cce5df
-        except KrbPrincipalExpired as e:
cce5df
-            return self.unauthorized(environ,
cce5df
-                                     start_response,
cce5df
-                                     str(e),
cce5df
-                                     'krbprincipal-expired')
cce5df
-        except UserLocked as e:
cce5df
-            return self.unauthorized(environ,
cce5df
-                                     start_response,
cce5df
-                                     str(e),
cce5df
-                                     'user-locked')
cce5df
+            result = attempt_kinit(user_principal, password,
cce5df
+                                   ipa_ccache_name, use_armor=True)
cce5df
+        except KrbPrincipalWrongFAST:
cce5df
+            result = attempt_kinit(user_principal, password,
cce5df
+                                   ipa_ccache_name, use_armor=False)
cce5df
+
cce5df
+        if result is not None:
cce5df
+            return result
cce5df
 
cce5df
         result = self.finalize_kerberos_acquisition('login_password',
cce5df
                                                     ipa_ccache_name, environ,
cce5df
@@ -1038,21 +1056,24 @@ class login_password(Backend, KerberosSession):
cce5df
             pass
cce5df
         return result
cce5df
 
cce5df
-    def kinit(self, principal, password, ccache_name):
cce5df
-        # get anonymous ccache as an armor for FAST to enable OTP auth
cce5df
-        armor_path = os.path.join(paths.IPA_CCACHES,
cce5df
-                                  "armor_{}".format(os.getpid()))
cce5df
+    def kinit(self, principal, password, ccache_name, use_armor=True):
cce5df
+        if use_armor:
cce5df
+            # get anonymous ccache as an armor for FAST to enable OTP auth
cce5df
+            armor_path = os.path.join(paths.IPA_CCACHES,
cce5df
+                                      "armor_{}".format(os.getpid()))
cce5df
 
cce5df
-        logger.debug('Obtaining armor in ccache %s', armor_path)
cce5df
+            logger.debug('Obtaining armor in ccache %s', armor_path)
cce5df
 
cce5df
-        try:
cce5df
-            kinit_armor(
cce5df
-                armor_path,
cce5df
-                pkinit_anchors=[paths.KDC_CERT, paths.KDC_CA_BUNDLE_PEM],
cce5df
-            )
cce5df
-        except RuntimeError as e:
cce5df
-            logger.error("Failed to obtain armor cache")
cce5df
-            # We try to continue w/o armor, 2FA will be impacted
cce5df
+            try:
cce5df
+                kinit_armor(
cce5df
+                    armor_path,
cce5df
+                    pkinit_anchors=[paths.KDC_CERT, paths.KDC_CA_BUNDLE_PEM],
cce5df
+                )
cce5df
+            except RuntimeError as e:
cce5df
+                logger.error("Failed to obtain armor cache")
cce5df
+                # We try to continue w/o armor, 2FA will be impacted
cce5df
+                armor_path = None
cce5df
+        else:
cce5df
             armor_path = None
cce5df
 
cce5df
         try:
cce5df
@@ -1080,6 +1101,9 @@ class login_password(Backend, KerberosSession):
cce5df
                   'while getting initial credentials') in str(e):
cce5df
                 raise UserLocked(principal=principal,
cce5df
                                  message=unicode(e))
cce5df
+            elif ('kinit: Error constructing AP-REQ armor: '
cce5df
+                  'Matching credential not found') in str(e):
cce5df
+                raise KrbPrincipalWrongFAST(principal=principal)
cce5df
             raise InvalidSessionPassword(principal=principal,
cce5df
                                          message=unicode(e))
cce5df
 
cce5df
diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py
cce5df
index a6a055c2a..bec918a31 100644
cce5df
--- a/ipatests/test_integration/test_trust.py
cce5df
+++ b/ipatests/test_integration/test_trust.py
cce5df
@@ -175,6 +175,27 @@ class TestTrust(BaseTestTrust):
cce5df
         tasks.kdestroy_all(self.master)
cce5df
         tasks.kinit_admin(self.master)
cce5df
 
cce5df
+    def test_password_login_as_aduser(self):
cce5df
+        """Test if AD user can login with password to Web UI"""
cce5df
+        ad_admin = 'Administrator@%s' % self.ad_domain
cce5df
+
cce5df
+        tasks.kdestroy_all(self.master)
cce5df
+        user_and_password = ('user=%s&password=%s' %
cce5df
+                             (ad_admin, self.master.config.ad_admin_password))
cce5df
+        host = self.master.hostname
cce5df
+        cmd_args = [
cce5df
+            paths.BIN_CURL,
cce5df
+            '-v',
cce5df
+            '-H', 'referer:https://{}/ipa'.format(host),
cce5df
+            '-H', 'Content-Type:application/x-www-form-urlencoded',
cce5df
+            '-H', 'Accept:text/plain',
cce5df
+            '--cacert', paths.IPA_CA_CRT,
cce5df
+            '--data', user_and_password,
cce5df
+            'https://{}/ipa/session/login_password'.format(host)]
cce5df
+        result = self.master.run_command(cmd_args)
cce5df
+        assert "Set-Cookie: ipa_session=MagBearerToken" in result.stdout_text
cce5df
+        tasks.kinit_admin(self.master)
cce5df
+
cce5df
     def test_ipauser_authentication_with_nonposix_trust(self):
cce5df
         ipauser = u'tuser'
cce5df
         original_passwd = 'Secret123'
cce5df
-- 
cce5df
2.29.2
cce5df