From c464cc0035a5552ab28b9e8c3eee7a5904f33996 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Wed, 20 Apr 2016 15:13:42 -0700 Subject: [PATCH] add support for principal and hostname override --- README.rst | 44 ++++++++++++++++++++++- requests_kerberos/kerberos_.py | 16 +++++++-- test_requests_kerberos.py | 64 +++++++++++++++++++++++++++++----- 3 files changed, 111 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 83183e2..09b0bf0 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,14 @@ authentication, you can do that as well: Preemptive Authentication ------------------------- -``HTTPKerberosAuth`` can be forced to preemptively initiate the Kerberos GSS exchange and present a Kerberos ticket on the initial request (and all subsequent). By default, authentication only occurs after a ``401 Unauthorized`` response containing a Kerberos or Negotiate challenge is received from the origin server. This can cause mutual authentication failures for hosts that use a persistent connection (eg, Windows/WinRM), as no Kerberos challenges are sent after the initial auth handshake. This behavior can be altered by setting ``force_preemptive=True``: +``HTTPKerberosAuth`` can be forced to preemptively initiate the Kerberos +GSS exchange and present a Kerberos ticket on the initial request (and all +subsequent). By default, authentication only occurs after a +``401 Unauthorized`` response containing a Kerberos or Negotiate challenge +is received from the origin server. This can cause mutual authentication +failures for hosts that use a persistent connection (eg, Windows/WinRM), as +no Kerberos challenges are sent after the initial auth handshake. This +behavior can be altered by setting ``force_preemptive=True``: .. code-block:: pycon @@ -75,6 +82,41 @@ Preemptive Authentication >>> r = requests.get("https://windows.example.org/wsman", auth=kerberos_auth) ... +Hostname Override +----------------- + +If communicating with a host whose DNS name doesn't match its +kerberos hostname (eg, behind a content switch or load balancer), +the hostname used for the Kerberos GSS exchange can be overridden by +setting the ``hostname_override`` arg: + +.. code-block:: pycon + + >>> import requests + >>> from requests_kerberos import HTTPKerberosAuth, REQUIRED + >>> kerberos_auth = HTTPKerberosAuth(hostname_override="internalhost.local") + >>> r = requests.get("https://externalhost.example.org/", auth=kerberos_auth) + ... + +Explicit Principal +------------------ + +``HTTPKerberosAuth`` normally uses the default principal (ie, the user for +whom you last ran ``kinit`` or ``kswitch``, or an SSO credential if +applicable). However, an explicit principal can be specified, which will +cause Kerberos to look for a matching credential cache for the named user. +This feature depends on OS support for collection-type credential caches, +as well as working principal support in pykerberos (it is broken in many +builds). An explicit principal can be specified with the ``principal`` arg: + +.. code-block:: pycon + + >>> import requests + >>> from requests_kerberos import HTTPKerberosAuth, REQUIRED + >>> kerberos_auth = HTTPKerberosAuth(principal="user@REALM") + >>> r = requests.get("http://example.org", auth=kerberos_auth) + ... + Logging ------- diff --git a/requests_kerberos/kerberos_.py b/requests_kerberos/kerberos_.py index 4aa44ed..4656d9d 100644 --- a/requests_kerberos/kerberos_.py +++ b/requests_kerberos/kerberos_.py @@ -84,13 +84,16 @@ class HTTPKerberosAuth(AuthBase): object.""" def __init__( self, mutual_authentication=REQUIRED, - service="HTTP", delegate=False, force_preemptive=False): + service="HTTP", delegate=False, force_preemptive=False, + principal=None, hostname_override=None): self.context = {} self.mutual_authentication = mutual_authentication self.delegate = delegate self.pos = None self.service = service self.force_preemptive = force_preemptive + self.principal = principal + self.hostname_override = hostname_override def generate_request_header(self, response, host, is_preemptive=False): """ @@ -108,8 +111,15 @@ class HTTPKerberosAuth(AuthBase): try: kerb_stage = "authGSSClientInit()" - result, self.context[host] = kerberos.authGSSClientInit( - "{0}@{1}".format(self.service, host), gssflags=gssflags) + # contexts still need to be stored by host, but hostname_override + # allows use of an arbitrary hostname for the kerberos exchange + # (eg, in cases of aliased hosts, internal vs external, CNAMEs + # w/ name-based HTTP hosting) + kerb_host = self.hostname_override if self.hostname_override is not None else host + kerb_spn = "{0}@{1}".format(self.service, kerb_host) + + result, self.context[host] = kerberos.authGSSClientInit(kerb_spn, + gssflags=gssflags, principal=self.principal) if result < 1: raise EnvironmentError(result, kerb_stage) diff --git a/test_requests_kerberos.py b/test_requests_kerberos.py index b31f644..45af2b1 100644 --- a/test_requests_kerberos.py +++ b/test_requests_kerberos.py @@ -119,7 +119,8 @@ class KerberosTestCase(unittest.TestCase): "HTTP@www.example.org", gssflags=( kerberos.GSS_C_MUTUAL_FLAG | - kerberos.GSS_C_SEQUENCE_FLAG)) + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) clientStep_continue.assert_called_with("CTX", "token") clientResponse.assert_called_with("CTX") @@ -140,7 +141,8 @@ class KerberosTestCase(unittest.TestCase): "HTTP@www.example.org", gssflags=( kerberos.GSS_C_MUTUAL_FLAG | - kerberos.GSS_C_SEQUENCE_FLAG)) + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) self.assertFalse(clientStep_continue.called) self.assertFalse(clientResponse.called) @@ -161,7 +163,8 @@ class KerberosTestCase(unittest.TestCase): "HTTP@www.example.org", gssflags=( kerberos.GSS_C_MUTUAL_FLAG | - kerberos.GSS_C_SEQUENCE_FLAG)) + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) clientStep_error.assert_called_with("CTX", "token") self.assertFalse(clientResponse.called) @@ -205,7 +208,8 @@ class KerberosTestCase(unittest.TestCase): "HTTP@www.example.org", gssflags=( kerberos.GSS_C_MUTUAL_FLAG | - kerberos.GSS_C_SEQUENCE_FLAG)) + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) clientStep_continue.assert_called_with("CTX", "token") clientResponse.assert_called_with("CTX") @@ -249,7 +253,8 @@ class KerberosTestCase(unittest.TestCase): "HTTP@www.example.org", gssflags=( kerberos.GSS_C_MUTUAL_FLAG | - kerberos.GSS_C_SEQUENCE_FLAG)) + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) clientStep_continue.assert_called_with("CTX", "token") clientResponse.assert_called_with("CTX") @@ -480,7 +485,8 @@ class KerberosTestCase(unittest.TestCase): "HTTP@www.example.org", gssflags=( kerberos.GSS_C_MUTUAL_FLAG | - kerberos.GSS_C_SEQUENCE_FLAG)) + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) clientStep_continue.assert_called_with("CTX", "token") clientResponse.assert_called_with("CTX") @@ -529,7 +535,8 @@ class KerberosTestCase(unittest.TestCase): "HTTP@www.example.org", gssflags=( kerberos.GSS_C_MUTUAL_FLAG | - kerberos.GSS_C_SEQUENCE_FLAG)) + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) clientStep_continue.assert_called_with("CTX", "token") clientResponse.assert_called_with("CTX") @@ -548,7 +555,8 @@ class KerberosTestCase(unittest.TestCase): "barfoo@www.example.org", gssflags=( kerberos.GSS_C_MUTUAL_FLAG | - kerberos.GSS_C_SEQUENCE_FLAG)) + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) def test_delegation(self): with patch.multiple('kerberos', @@ -591,10 +599,48 @@ class KerberosTestCase(unittest.TestCase): gssflags=( kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG | - kerberos.GSS_C_DELEG_FLAG)) + kerberos.GSS_C_DELEG_FLAG), + principal=None + ) clientStep_continue.assert_called_with("CTX", "token") clientResponse.assert_called_with("CTX") + def test_principal_override(self): + with patch.multiple(kerberos_module_name, + authGSSClientInit=clientInit_complete, + authGSSClientResponse=clientResponse, + authGSSClientStep=clientStep_continue): + response = requests.Response() + response.url = "http://www.example.org/" + response.headers = {'www-authenticate': 'negotiate token'} + host = urlparse(response.url).hostname + auth = requests_kerberos.HTTPKerberosAuth(principal="user@REALM") + auth.generate_request_header(response, host), + clientInit_complete.assert_called_with( + "HTTP@www.example.org", + gssflags=( + kerberos.GSS_C_MUTUAL_FLAG | + kerberos.GSS_C_SEQUENCE_FLAG), + principal="user@REALM") + + def test_realm_override(self): + with patch.multiple(kerberos_module_name, + authGSSClientInit=clientInit_complete, + authGSSClientResponse=clientResponse, + authGSSClientStep=clientStep_continue): + response = requests.Response() + response.url = "http://www.example.org/" + response.headers = {'www-authenticate': 'negotiate token'} + host = urlparse(response.url).hostname + auth = requests_kerberos.HTTPKerberosAuth(hostname_override="otherhost.otherdomain.org") + auth.generate_request_header(response, host), + clientInit_complete.assert_called_with( + "HTTP@otherhost.otherdomain.org", + gssflags=( + kerberos.GSS_C_MUTUAL_FLAG | + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) + if __name__ == '__main__': unittest.main()