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