From cd008bf83078853d07a8e152ec82915a65a2254a Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Wed, 22 Nov 2017 17:23:30 -0500 Subject: [PATCH 1/8] Add HTTPSPNEGOAuth (and retain HTTPKerberosAuth as a shim) Signed-off-by: Robbie Harwood Resolves: #1 --- README.rst | 44 +++++++++++++++++------------------ requests_gssapi/__init__.py | 13 +++++++---- requests_gssapi/gssapi_.py | 6 ++--- setup.py | 2 +- test_requests_gssapi.py | 46 ++++++++++++++++++------------------- 5 files changed, 57 insertions(+), 54 deletions(-) diff --git a/README.rst b/README.rst index d1c0fa6..b2f255d 100644 --- a/README.rst +++ b/README.rst @@ -9,8 +9,8 @@ authentication. Basic GET usage: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth - >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth()) + >>> from requests_gssapi import HTTPSPNEGOAuth + >>> r = requests.get("http://example.org", auth=HTTPSPNEGOAuth()) ... The entire ``requests.api`` should be supported. @@ -27,7 +27,7 @@ Mutual Authentication REQUIRED ^^^^^^^^ -By default, ``HTTPKerberosAuth`` will require mutual authentication from the +By default, ``HTTPSPNEGOAuth`` will require mutual authentication from the server, and if a server emits a non-error response which cannot be authenticated, a ``requests_gssapi.errors.MutualAuthenticationError`` will be raised. If a server emits an error which cannot be authenticated, it will @@ -39,8 +39,8 @@ setting ``sanitize_mutual_error_response=False``: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth, REQUIRED - >>> gssapi_auth = HTTPKerberosAuth(mutual_authentication=REQUIRED, sanitize_mutual_error_response=False) + >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED + >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=REQUIRED, sanitize_mutual_error_response=False) >>> r = requests.get("https://windows.example.org/wsman", auth=gssapi_auth) ... @@ -49,13 +49,13 @@ OPTIONAL ^^^^^^^^ If you'd prefer to not require mutual authentication, you can set your -preference when constructing your ``HTTPKerberosAuth`` object: +preference when constructing your ``HTTPSPNEGOAuth`` object: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth, OPTIONAL - >>> gssapi_auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL) + >>> from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL + >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL) >>> r = requests.get("http://example.org", auth=gssapi_auth) ... @@ -72,15 +72,15 @@ authentication, you can do that as well: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth, DISABLED - >>> gssapi_auth = HTTPKerberosAuth(mutual_authentication=DISABLED) + >>> from requests_gssapi import HTTPSPNEGOAuth, DISABLED + >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=DISABLED) >>> r = requests.get("http://example.org", auth=gssapi_auth) ... Preemptive Authentication ------------------------- -``HTTPKerberosAuth`` can be forced to preemptively initiate the GSSAPI +``HTTPSPNEGOAuth`` can be forced to preemptively initiate the GSSAPI exchange and present a token on the initial request (and all subsequent). By default, authentication only occurs after a ``401 Unauthorized`` response containing a Negotiate challenge @@ -92,8 +92,8 @@ behavior can be altered by setting ``force_preemptive=True``: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth, REQUIRED - >>> gssapi_auth = HTTPKerberosAuth(mutual_authentication=REQUIRED, force_preemptive=True) + >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED + >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=REQUIRED, force_preemptive=True) >>> r = requests.get("https://windows.example.org/wsman", auth=gssapi_auth) ... @@ -108,15 +108,15 @@ setting the ``hostname_override`` arg: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth, REQUIRED - >>> gssapi_auth = HTTPKerberosAuth(hostname_override="internalhost.local") - >>> r = requests.get("https://externalhost.example.org/", auth=kerberos_auth) + >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED + >>> gssapi_auth = HTTPSPNEGOAuth(hostname_override="internalhost.local") + >>> r = requests.get("https://externalhost.example.org/", auth=gssapi_auth) ... Explicit Principal ------------------ -``HTTPKerberosAuth`` normally uses the default principal (ie, the user for +``HTTPSPNEGOAuth`` 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 GSSAPI to look for a matching credential cache for the named user. @@ -126,8 +126,8 @@ An explicit principal can be specified with the ``principal`` arg: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth, REQUIRED - >>> gssapi_auth = HTTPKerberosAuth(principal="user@REALM") + >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED + >>> gssapi_auth = HTTPSPNEGOAuth(principal="user@REALM") >>> r = requests.get("http://example.org", auth=gssapi_auth) ... @@ -136,13 +136,13 @@ Delegation ``requests_gssapi`` supports credential delegation (``GSS_C_DELEG_FLAG``). To enable delegation of credentials to a server that requests delegation, pass -``delegate=True`` to ``HTTPKerberosAuth``: +``delegate=True`` to ``HTTPSPNEGOAuth``: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth - >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth(delegate=True)) + >>> from requests_gssapi import HTTPSPNEGOAuth + >>> r = requests.get("http://example.org", auth=HTTPSPNEGOAuth(delegate=True)) ... Be careful to only allow delegation to servers you trust as they will be able diff --git a/requests_gssapi/__init__.py b/requests_gssapi/__init__.py index 2e43839..479bcde 100644 --- a/requests_gssapi/__init__.py +++ b/requests_gssapi/__init__.py @@ -7,19 +7,22 @@ authentication. Basic GET usage: >>> import requests - >>> from requests_gssapi import HTTPKerberosAuth - >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth()) + >>> from requests_gssapi import HTTPSPNEGOAuth + >>> r = requests.get("http://example.org", auth=HTTPSPNEGOAuth()) The entire `requests.api` should be supported. """ import logging -from .gssapi_ import HTTPKerberosAuth, REQUIRED, OPTIONAL, DISABLED +from .gssapi_ import HTTPSPNEGOAuth, REQUIRED, OPTIONAL, DISABLED from .exceptions import MutualAuthenticationError from .compat import NullHandler logging.getLogger(__name__).addHandler(NullHandler()) -__all__ = ('HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', - 'OPTIONAL', 'DISABLED') +""" Deprecated compatability shim """ +HTTPKerberosAuth = HTTPSPNEGOAuth + +__all__ = ('HTTPKerberosAuth', 'HTTPSNPEGOAuth', 'MutualAuthenticationError', + 'REQUIRED', 'OPTIONAL', 'DISABLED') __version__ = '0.11.0' diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index 001bfcf..9dc8f62 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -78,7 +78,7 @@ def _negotiate_value(response): return None -class HTTPKerberosAuth(AuthBase): +class HTTPSPNEGOAuth(AuthBase): """Attaches HTTP GSSAPI Authentication to the given Request object.""" def __init__(self, mutual_authentication=REQUIRED, service="HTTP", delegate=False, force_preemptive=False, principal=None, @@ -292,7 +292,7 @@ def __call__(self, request): is_preemptive=True) log.debug( - "HTTPKerberosAuth: Preemptive Authorization header: {0}" + "HTTPSPNEGOAuth: Preemptive Authorization header: {0}" .format(auth_header)) request.headers['Authorization'] = auth_header @@ -301,7 +301,7 @@ def __call__(self, request): try: self.pos = request.body.tell() except AttributeError: - # In the case of HTTPKerberosAuth being reused and the body + # In the case of HTTPSPNEGOAuth being reused and the body # of the previous request was a file-like object, pos has # the file position of the previous body. Ensure it's set to # None. diff --git a/setup.py b/setup.py index ea7bd15..3e97a13 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def get_version(): long_description=long_desc, author='Ian Cordasco, Cory Benfield, Michael Komitee, Robbie Harwood', author_email='rharwood@redhat.com', - url='https://github.com/frozencemetery/requests-gssapi', + url='https://github.com/pythongssapi/requests-gssapi', packages=['requests_gssapi'], package_data={'': ['LICENSE', 'AUTHORS']}, include_package_data=True, diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index a5fd20b..1948c22 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -59,7 +59,7 @@ def test_negotate_value_extraction_none(self): def test_force_preemptive(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, step=fake_resp): - auth = requests_gssapi.HTTPKerberosAuth(force_preemptive=True) + auth = requests_gssapi.HTTPSPNEGOAuth(force_preemptive=True) request = requests.Request(url="http://www.example.org") @@ -72,7 +72,7 @@ def test_force_preemptive(self): def test_no_force_preemptive(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, step=fake_resp): - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() request = requests.Request(url="http://www.example.org") @@ -87,7 +87,7 @@ def test_generate_request_header(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() self.assertEqual( auth.generate_request_header(response, host), "Negotiate GSSRESPONSE") @@ -103,7 +103,7 @@ def test_generate_request_header_init_error(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() self.assertRaises(requests_gssapi.exceptions.KerberosExchangeError, auth.generate_request_header, response, host) fake_init.assert_called_with( @@ -117,7 +117,7 @@ def test_generate_request_header_step_error(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() self.assertRaises(requests_gssapi.exceptions.KerberosExchangeError, auth.generate_request_header, response, host) fake_init.assert_called_with( @@ -148,7 +148,7 @@ def test_authenticate_user(self): response.connection = connection response._content = "" response.raw = raw - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() r = auth.authenticate_user(response) self.assertTrue(response in r.history) @@ -185,7 +185,7 @@ def test_handle_401(self): response.connection = connection response._content = "" response.raw = raw - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() r = auth.handle_401(response) self.assertTrue(response in r.history) @@ -209,7 +209,7 @@ def test_authenticate_server(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() auth.context = {"www.example.org": gssapi.SecurityContext} result = auth.authenticate_server(response_ok) @@ -226,7 +226,7 @@ def test_handle_other(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() auth.context = {"www.example.org": gssapi.SecurityContext} r = auth.handle_other(response_ok) @@ -244,7 +244,7 @@ def test_handle_response_200(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() auth.context = {"www.example.org": gssapi.SecurityContext} r = auth.handle_response(response_ok) @@ -261,7 +261,7 @@ def test_handle_response_200_mutual_auth_required_failure(self): response_ok.status_code = 200 response_ok.headers = {} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() auth.context = {"www.example.org": "CTX"} self.assertRaises(requests_gssapi.MutualAuthenticationError, @@ -279,7 +279,7 @@ def test_handle_response_200_mutual_auth_required_failure_2(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() auth.context = {"www.example.org": gssapi.SecurityContext} self.assertRaises(requests_gssapi.MutualAuthenticationError, @@ -297,7 +297,7 @@ def test_handle_response_200_mutual_auth_optional_hard_failure(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPKerberosAuth( + auth = requests_gssapi.HTTPSPNEGOAuth( requests_gssapi.OPTIONAL) auth.context = {"www.example.org": gssapi.SecurityContext} @@ -313,7 +313,7 @@ def test_handle_response_200_mutual_auth_optional_soft_failure(self): response_ok.url = "http://www.example.org/" response_ok.status_code = 200 - auth = requests_gssapi.HTTPKerberosAuth( + auth = requests_gssapi.HTTPSPNEGOAuth( requests_gssapi.OPTIONAL) auth.context = {"www.example.org": gssapi.SecurityContext} @@ -337,7 +337,7 @@ def test_handle_response_500_mutual_auth_required_failure(self): response_500.raw = "RAW" response_500.cookies = "COOKIES" - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() auth.context = {"www.example.org": "CTX"} r = auth.handle_response(response_500) @@ -358,7 +358,7 @@ def test_handle_response_500_mutual_auth_required_failure(self): self.assertFalse(fail_resp.called) # re-test with error response sanitizing disabled - auth = requests_gssapi.HTTPKerberosAuth( + auth = requests_gssapi.HTTPSPNEGOAuth( sanitize_mutual_error_response=False) auth.context = {"www.example.org": "CTX"} @@ -381,7 +381,7 @@ def test_handle_response_500_mutual_auth_optional_failure(self): response_500.raw = "RAW" response_500.cookies = "COOKIES" - auth = requests_gssapi.HTTPKerberosAuth( + auth = requests_gssapi.HTTPSPNEGOAuth( requests_gssapi.OPTIONAL) auth.context = {"www.example.org": "CTX"} @@ -416,7 +416,7 @@ def test_handle_response_401(self): response._content = "" response.raw = raw - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() auth.handle_other = Mock(return_value=response_ok) r = auth.handle_response(response) @@ -462,7 +462,7 @@ def connection_send(self, *args, **kwargs): response._content = "" response.raw = raw - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPSPNEGOAuth() r = auth.handle_response(response) @@ -483,7 +483,7 @@ def test_generate_request_header_custom_service(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPKerberosAuth(service="barfoo") + auth = requests_gssapi.HTTPSPNEGOAuth(service="barfoo") auth.generate_request_header(response, host), fake_init.assert_called_with( name=gssapi.Name("barfoo@www.example.org"), @@ -513,7 +513,7 @@ def test_delegation(self): response.connection = connection response._content = "" response.raw = raw - auth = requests_gssapi.HTTPKerberosAuth(1, "HTTP", True) + auth = requests_gssapi.HTTPSPNEGOAuth(1, "HTTP", True) r = auth.authenticate_user(response) self.assertTrue(response in r.history) @@ -535,7 +535,7 @@ def test_principal_override(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPKerberosAuth(principal="user@REALM") + auth = requests_gssapi.HTTPSPNEGOAuth(principal="user@REALM") auth.generate_request_header(response, host) fake_creds.assert_called_with(gssapi.creds.Credentials, usage="initiate", @@ -552,7 +552,7 @@ def test_realm_override(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPKerberosAuth( + auth = requests_gssapi.HTTPSPNEGOAuth( hostname_override="otherhost.otherdomain.org") auth.generate_request_header(response, host) fake_init.assert_called_with( From 5242673cc0c2d05695c6e8ea1b18b19791698425 Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Wed, 22 Nov 2017 17:28:08 -0500 Subject: [PATCH 2/8] Add SPNEGOExchangeError (and retain KerberosExchangeError as a shim) Signed-off-by: Robbie Harwood Resolves: #1 --- requests_gssapi/exceptions.py | 8 ++++++-- requests_gssapi/gssapi_.py | 10 +++++----- test_requests_gssapi.py | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/requests_gssapi/exceptions.py b/requests_gssapi/exceptions.py index 15a5f30..a13c400 100644 --- a/requests_gssapi/exceptions.py +++ b/requests_gssapi/exceptions.py @@ -12,5 +12,9 @@ class MutualAuthenticationError(RequestException): """Mutual Authentication Error""" -class KerberosExchangeError(RequestException): - """Kerberos Exchange Failed Error""" +class SPNEGOExchangeError(RequestException): + """SPNEGO Exchange Failed Error""" + + +""" Deprecated compatability shim """ +KerberosExchangeError = SPNEGOExchangeError diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index 9dc8f62..d8d54f9 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -9,7 +9,7 @@ from requests.structures import CaseInsensitiveDict from requests.cookies import cookiejar_from_dict -from .exceptions import MutualAuthenticationError, KerberosExchangeError +from .exceptions import MutualAuthenticationError, SPNEGOExchangeError log = logging.getLogger(__name__) @@ -95,9 +95,9 @@ def __init__(self, mutual_authentication=REQUIRED, service="HTTP", def generate_request_header(self, response, host, is_preemptive=False): """ - Generates the GSSAPI authentication token with kerberos. + Generates the GSSAPI authentication token - If any GSSAPI step fails, raise KerberosExchangeError + If any GSSAPI step fails, raise SPNEGOExchangeError with failure detail. """ @@ -143,7 +143,7 @@ def generate_request_header(self, response, host, is_preemptive=False): log.exception( "generate_request_header(): {0} failed:".format(gss_stage)) log.exception(msg) - raise KerberosExchangeError("%s failed: %s" % (gss_stage, msg)) + raise SPNEGOExchangeError("%s failed: %s" % (gss_stage, msg)) def authenticate_user(self, response, **kwargs): """Handles user authentication with GSSAPI""" @@ -152,7 +152,7 @@ def authenticate_user(self, response, **kwargs): try: auth_header = self.generate_request_header(response, host) - except KerberosExchangeError: + except SPNEGOExchangeError: # GSS Failure, return existing response return response diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index 1948c22..d783cad 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -104,7 +104,7 @@ def test_generate_request_header_init_error(self): response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname auth = requests_gssapi.HTTPSPNEGOAuth() - self.assertRaises(requests_gssapi.exceptions.KerberosExchangeError, + self.assertRaises(requests_gssapi.exceptions.SPNEGOExchangeError, auth.generate_request_header, response, host) fake_init.assert_called_with( name=gssapi.Name("HTTP@www.example.org"), @@ -118,7 +118,7 @@ def test_generate_request_header_step_error(self): response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname auth = requests_gssapi.HTTPSPNEGOAuth() - self.assertRaises(requests_gssapi.exceptions.KerberosExchangeError, + self.assertRaises(requests_gssapi.exceptions.SPNEGOExchangeError, auth.generate_request_header, response, host) fake_init.assert_called_with( name=gssapi.Name("HTTP@www.example.org"), From 5d0e8fa4f7b9092d30efcb2b884a20b08aef61e8 Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Wed, 22 Nov 2017 17:31:01 -0500 Subject: [PATCH 3/8] Remove final mentions of Kerberos in source tree Signed-off-by: Robbie Harwood --- test_requests_gssapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index d783cad..c46f02e 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -30,7 +30,7 @@ gssdelegflags = gssflags + [gssapi.RequirementFlag.delegate_to_peer] -class KerberosTestCase(unittest.TestCase): +class GSSAPITestCase(unittest.TestCase): def setUp(self): """Setup.""" fake_init.reset_mock() From c7804faa945ad1b77ba4060531c9f410a5a0ffdd Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Thu, 7 Dec 2017 15:44:32 -0500 Subject: [PATCH 4/8] Make shim into its own wrapper subclass Signed-off-by: Robbie Harwood --- requests_gssapi/__init__.py | 9 +++----- requests_gssapi/compat.py | 26 ++++++++++++++++++++- test_requests_gssapi.py | 46 ++++++++++++++++++------------------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/requests_gssapi/__init__.py b/requests_gssapi/__init__.py index 479bcde..4c69401 100644 --- a/requests_gssapi/__init__.py +++ b/requests_gssapi/__init__.py @@ -14,15 +14,12 @@ """ import logging -from .gssapi_ import HTTPSPNEGOAuth, REQUIRED, OPTIONAL, DISABLED +from .gssapi_ import HTTPSPNEGOAuth, REQUIRED, OPTIONAL, DISABLED # noqa from .exceptions import MutualAuthenticationError -from .compat import NullHandler +from .compat import NullHandler, HTTPKerberosAuth logging.getLogger(__name__).addHandler(NullHandler()) -""" Deprecated compatability shim """ -HTTPKerberosAuth = HTTPSPNEGOAuth - -__all__ = ('HTTPKerberosAuth', 'HTTPSNPEGOAuth', 'MutualAuthenticationError', +__all__ = ('HTTPSPNEGOAuth', 'HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', 'OPTIONAL', 'DISABLED') __version__ = '0.11.0' diff --git a/requests_gssapi/compat.py b/requests_gssapi/compat.py index d969fda..302b0d0 100644 --- a/requests_gssapi/compat.py +++ b/requests_gssapi/compat.py @@ -1,8 +1,10 @@ """ -Compatibility library for older versions of python +Compatibility library for older versions of python and requests_kerberos """ import sys +from .gssapi_ import REQUIRED, HTTPSPNEGOAuth # noqa + # python 2.7 introduced a NullHandler which we want to use, but to support # older versions, we implement our own if needed. if sys.version_info[:2] > (2, 6): @@ -13,3 +15,25 @@ class NullHandler(Handler): def emit(self, record): pass + + +class HTTPKerberosAuth(HTTPSPNEGOAuth): + """Deprecated compat shim; see HTTPSPNEGOAuth instead.""" + def __init__(self, mutual_authentication=REQUIRED, service="HTTP", + delegate=False, force_preemptive=False, principal=None, + hostname_override=None, sanitize_mutual_error_response=True): + HTTPSPNEGOAuth.__init__( + self, + mutual_authentication=mutual_authentication, + service=service, + delegate=delegate, + force_preemptive=force_preemptive, + principal=principal, + hostname_override=hostname_override, + sanitize_mutual_error_response=sanitize_mutual_error_response) + + def generate_request_header(self, response, host, is_preemptive=False): + # This method needs to be shimmed because `host` isn't exposed to + # __init__() and we need to derive things from it + return HTTPSPNEGOAuth.generate_request_header(self, response, host, + is_preemptive) diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index c46f02e..54a03bf 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -59,7 +59,7 @@ def test_negotate_value_extraction_none(self): def test_force_preemptive(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, step=fake_resp): - auth = requests_gssapi.HTTPSPNEGOAuth(force_preemptive=True) + auth = requests_gssapi.HTTPKerberosAuth(force_preemptive=True) request = requests.Request(url="http://www.example.org") @@ -72,7 +72,7 @@ def test_force_preemptive(self): def test_no_force_preemptive(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, step=fake_resp): - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() request = requests.Request(url="http://www.example.org") @@ -87,7 +87,7 @@ def test_generate_request_header(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() self.assertEqual( auth.generate_request_header(response, host), "Negotiate GSSRESPONSE") @@ -103,7 +103,7 @@ def test_generate_request_header_init_error(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() self.assertRaises(requests_gssapi.exceptions.SPNEGOExchangeError, auth.generate_request_header, response, host) fake_init.assert_called_with( @@ -117,7 +117,7 @@ def test_generate_request_header_step_error(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() self.assertRaises(requests_gssapi.exceptions.SPNEGOExchangeError, auth.generate_request_header, response, host) fake_init.assert_called_with( @@ -148,7 +148,7 @@ def test_authenticate_user(self): response.connection = connection response._content = "" response.raw = raw - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() r = auth.authenticate_user(response) self.assertTrue(response in r.history) @@ -185,7 +185,7 @@ def test_handle_401(self): response.connection = connection response._content = "" response.raw = raw - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() r = auth.handle_401(response) self.assertTrue(response in r.history) @@ -209,7 +209,7 @@ def test_authenticate_server(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": gssapi.SecurityContext} result = auth.authenticate_server(response_ok) @@ -226,7 +226,7 @@ def test_handle_other(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": gssapi.SecurityContext} r = auth.handle_other(response_ok) @@ -244,7 +244,7 @@ def test_handle_response_200(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": gssapi.SecurityContext} r = auth.handle_response(response_ok) @@ -261,7 +261,7 @@ def test_handle_response_200_mutual_auth_required_failure(self): response_ok.status_code = 200 response_ok.headers = {} - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": "CTX"} self.assertRaises(requests_gssapi.MutualAuthenticationError, @@ -279,7 +279,7 @@ def test_handle_response_200_mutual_auth_required_failure_2(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": gssapi.SecurityContext} self.assertRaises(requests_gssapi.MutualAuthenticationError, @@ -297,7 +297,7 @@ def test_handle_response_200_mutual_auth_optional_hard_failure(self): 'www-authenticate': 'negotiate servertoken', 'authorization': 'Negotiate GSSRESPONSE'} - auth = requests_gssapi.HTTPSPNEGOAuth( + auth = requests_gssapi.HTTPKerberosAuth( requests_gssapi.OPTIONAL) auth.context = {"www.example.org": gssapi.SecurityContext} @@ -313,7 +313,7 @@ def test_handle_response_200_mutual_auth_optional_soft_failure(self): response_ok.url = "http://www.example.org/" response_ok.status_code = 200 - auth = requests_gssapi.HTTPSPNEGOAuth( + auth = requests_gssapi.HTTPKerberosAuth( requests_gssapi.OPTIONAL) auth.context = {"www.example.org": gssapi.SecurityContext} @@ -337,7 +337,7 @@ def test_handle_response_500_mutual_auth_required_failure(self): response_500.raw = "RAW" response_500.cookies = "COOKIES" - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": "CTX"} r = auth.handle_response(response_500) @@ -358,7 +358,7 @@ def test_handle_response_500_mutual_auth_required_failure(self): self.assertFalse(fail_resp.called) # re-test with error response sanitizing disabled - auth = requests_gssapi.HTTPSPNEGOAuth( + auth = requests_gssapi.HTTPKerberosAuth( sanitize_mutual_error_response=False) auth.context = {"www.example.org": "CTX"} @@ -381,7 +381,7 @@ def test_handle_response_500_mutual_auth_optional_failure(self): response_500.raw = "RAW" response_500.cookies = "COOKIES" - auth = requests_gssapi.HTTPSPNEGOAuth( + auth = requests_gssapi.HTTPKerberosAuth( requests_gssapi.OPTIONAL) auth.context = {"www.example.org": "CTX"} @@ -416,7 +416,7 @@ def test_handle_response_401(self): response._content = "" response.raw = raw - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() auth.handle_other = Mock(return_value=response_ok) r = auth.handle_response(response) @@ -462,7 +462,7 @@ def connection_send(self, *args, **kwargs): response._content = "" response.raw = raw - auth = requests_gssapi.HTTPSPNEGOAuth() + auth = requests_gssapi.HTTPKerberosAuth() r = auth.handle_response(response) @@ -483,7 +483,7 @@ def test_generate_request_header_custom_service(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPSPNEGOAuth(service="barfoo") + auth = requests_gssapi.HTTPKerberosAuth(service="barfoo") auth.generate_request_header(response, host), fake_init.assert_called_with( name=gssapi.Name("barfoo@www.example.org"), @@ -513,7 +513,7 @@ def test_delegation(self): response.connection = connection response._content = "" response.raw = raw - auth = requests_gssapi.HTTPSPNEGOAuth(1, "HTTP", True) + auth = requests_gssapi.HTTPKerberosAuth(1, "HTTP", True) r = auth.authenticate_user(response) self.assertTrue(response in r.history) @@ -535,7 +535,7 @@ def test_principal_override(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPSPNEGOAuth(principal="user@REALM") + auth = requests_gssapi.HTTPKerberosAuth(principal="user@REALM") auth.generate_request_header(response, host) fake_creds.assert_called_with(gssapi.creds.Credentials, usage="initiate", @@ -552,7 +552,7 @@ def test_realm_override(self): response.url = "http://www.example.org/" response.headers = {'www-authenticate': 'negotiate token'} host = urlparse(response.url).hostname - auth = requests_gssapi.HTTPSPNEGOAuth( + auth = requests_gssapi.HTTPKerberosAuth( hostname_override="otherhost.otherdomain.org") auth.generate_request_header(response, host) fake_init.assert_called_with( From b2de74634326fa8b923f514bf54f83b0c728ef8a Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Thu, 7 Dec 2017 15:58:11 -0500 Subject: [PATCH 5/8] Rename force_preemptive to opportunistic_auth Signed-off-by: Robbie Harwood --- requests_gssapi/compat.py | 2 +- requests_gssapi/gssapi_.py | 6 +++--- test_requests_gssapi.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/requests_gssapi/compat.py b/requests_gssapi/compat.py index 302b0d0..093400d 100644 --- a/requests_gssapi/compat.py +++ b/requests_gssapi/compat.py @@ -27,7 +27,7 @@ def __init__(self, mutual_authentication=REQUIRED, service="HTTP", mutual_authentication=mutual_authentication, service=service, delegate=delegate, - force_preemptive=force_preemptive, + opportunistic_auth=force_preemptive, principal=principal, hostname_override=hostname_override, sanitize_mutual_error_response=sanitize_mutual_error_response) diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index d8d54f9..abfae2b 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -81,14 +81,14 @@ def _negotiate_value(response): class HTTPSPNEGOAuth(AuthBase): """Attaches HTTP GSSAPI Authentication to the given Request object.""" def __init__(self, mutual_authentication=REQUIRED, service="HTTP", - delegate=False, force_preemptive=False, principal=None, + delegate=False, opportunistic_auth=False, principal=None, hostname_override=None, sanitize_mutual_error_response=True): self.context = {} self.mutual_authentication = mutual_authentication self.delegate = delegate self.pos = None self.service = service - self.force_preemptive = force_preemptive + self.opportunistic_auth = opportunistic_auth self.principal = principal self.hostname_override = hostname_override self.sanitize_mutual_error_response = sanitize_mutual_error_response @@ -283,7 +283,7 @@ def deregister(self, response): response.request.deregister_hook('response', self.handle_response) def __call__(self, request): - if self.force_preemptive: + if self.opportunistic_auth: # add Authorization header before we receive a 401 # by the 401 handler host = urlparse(request.url).hostname diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index 54a03bf..5d6d61d 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -560,6 +560,19 @@ def test_realm_override(self): usage="initiate", flags=gssflags, creds=None) fake_resp.assert_called_with("token") + def test_opportunistic_auth(self): + with patch.multiple("gssapi.SecurityContext", __init__=fake_init, + step=fake_resp): + auth = requests_gssapi.HTTPSPNEGOAuth(opportunistic_auth=True) + + request = requests.Request(url="http://www.example.org") + + auth.__call__(request) + + self.assertTrue('Authorization' in request.headers) + self.assertEqual(request.headers.get('Authorization'), + 'Negotiate GSSRESPONSE') + if __name__ == '__main__': unittest.main() From cdcc45ba93e1a7a27f118550280b00dfc7a28a32 Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Thu, 7 Dec 2017 16:26:48 -0500 Subject: [PATCH 6/8] Allow passing in creds explicitly (deprecates principal) Signed-off-by: Robbie Harwood --- requests_gssapi/compat.py | 28 +++++++++++++++++++++++----- requests_gssapi/gssapi_.py | 12 +++--------- test_requests_gssapi.py | 16 ++++++++++++++++ 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/requests_gssapi/compat.py b/requests_gssapi/compat.py index 093400d..768f8d5 100644 --- a/requests_gssapi/compat.py +++ b/requests_gssapi/compat.py @@ -3,7 +3,9 @@ """ import sys -from .gssapi_ import REQUIRED, HTTPSPNEGOAuth # noqa +import gssapi + +from .gssapi_ import REQUIRED, HTTPSPNEGOAuth, SPNEGOExchangeError, log # python 2.7 introduced a NullHandler which we want to use, but to support # older versions, we implement our own if needed. @@ -22,18 +24,34 @@ class HTTPKerberosAuth(HTTPSPNEGOAuth): def __init__(self, mutual_authentication=REQUIRED, service="HTTP", delegate=False, force_preemptive=False, principal=None, hostname_override=None, sanitize_mutual_error_response=True): + # put this here for later + self.principal = principal + HTTPSPNEGOAuth.__init__( self, mutual_authentication=mutual_authentication, service=service, delegate=delegate, opportunistic_auth=force_preemptive, - principal=principal, + creds=None, hostname_override=hostname_override, sanitize_mutual_error_response=sanitize_mutual_error_response) def generate_request_header(self, response, host, is_preemptive=False): # This method needs to be shimmed because `host` isn't exposed to - # __init__() and we need to derive things from it - return HTTPSPNEGOAuth.generate_request_header(self, response, host, - is_preemptive) + # __init__() and we need to derive things from it. Also, __init__() + # can't fail, in the strictest compatability sense. + try: + if self.principal is not None: + gss_stage = "acquiring credentials" + name = gssapi.Name(self.principal) + self.creds = gssapi.Credentials(name=name, usage="initiate") + + return HTTPSPNEGOAuth.generate_request_header(self, response, + host, is_preemptive) + except gssapi.exceptions.GSSError as error: + msg = error.gen_message() + log.exception( + "generate_request_header(): {0} failed:".format(gss_stage)) + log.exception(msg) + raise SPNEGOExchangeError("%s failed: %s" % (gss_stage, msg)) diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index abfae2b..9942e1e 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -81,7 +81,7 @@ def _negotiate_value(response): class HTTPSPNEGOAuth(AuthBase): """Attaches HTTP GSSAPI Authentication to the given Request object.""" def __init__(self, mutual_authentication=REQUIRED, service="HTTP", - delegate=False, opportunistic_auth=False, principal=None, + delegate=False, opportunistic_auth=False, creds=None, hostname_override=None, sanitize_mutual_error_response=True): self.context = {} self.mutual_authentication = mutual_authentication @@ -89,7 +89,7 @@ def __init__(self, mutual_authentication=REQUIRED, service="HTTP", self.pos = None self.service = service self.opportunistic_auth = opportunistic_auth - self.principal = principal + self.creds = creds self.hostname_override = hostname_override self.sanitize_mutual_error_response = sanitize_mutual_error_response @@ -118,16 +118,10 @@ def generate_request_header(self, response, host, is_preemptive=False): kerb_spn = "{0}@{1}".format(self.service, kerb_host) - creds = None - if self.principal: - gss_stage = "acquiring credentials" - creds = gssapi.Credentials(name=gssapi.Name(self.principal), - usage="initiate") - gss_stage = "initiating context" self.context[host] = gssapi.SecurityContext( usage="initiate", flags=gssflags, name=gssapi.Name(kerb_spn), - creds=creds) + creds=self.creds) gss_stage = "stepping context" if is_preemptive: diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index 5d6d61d..003fae1 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -573,6 +573,22 @@ def test_opportunistic_auth(self): self.assertEqual(request.headers.get('Authorization'), 'Negotiate GSSRESPONSE') + def test_explicit_creds(self): + with patch.multiple("gssapi.Credentials", __new__=fake_creds), \ + patch.multiple("gssapi.SecurityContext", __init__=fake_init, + step=fake_resp): + response = requests.Response() + response.url = "http://www.example.org/" + response.headers = {'www-authenticate': 'negotiate token'} + host = urlparse(response.url).hostname + creds = gssapi.Credentials() + auth = requests_gssapi.HTTPSPNEGOAuth(creds=creds) + auth.generate_request_header(response, host) + fake_init.assert_called_with( + name=gssapi.Name("HTTP@www.example.org"), + usage="initiate", flags=gssflags, creds="fake creds") + fake_resp.assert_called_with("token") + if __name__ == '__main__': unittest.main() From 9507b8b4476c9bd77c1a554d10df8e4a81862d4d Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Thu, 7 Dec 2017 16:42:05 -0500 Subject: [PATCH 7/8] Deprecate service and hostname_override in favor of explicit names Signed-off-by: Robbie Harwood --- requests_gssapi/compat.py | 20 +++++++++++++++++--- requests_gssapi/gssapi_.py | 26 ++++++++++---------------- test_requests_gssapi.py | 15 +++++++++++++++ 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/requests_gssapi/compat.py b/requests_gssapi/compat.py index 768f8d5..ed18142 100644 --- a/requests_gssapi/compat.py +++ b/requests_gssapi/compat.py @@ -24,17 +24,18 @@ class HTTPKerberosAuth(HTTPSPNEGOAuth): def __init__(self, mutual_authentication=REQUIRED, service="HTTP", delegate=False, force_preemptive=False, principal=None, hostname_override=None, sanitize_mutual_error_response=True): - # put this here for later + # put these here for later self.principal = principal + self.service = service + self.hostname_override = hostname_override HTTPSPNEGOAuth.__init__( self, mutual_authentication=mutual_authentication, - service=service, + target_name=None, delegate=delegate, opportunistic_auth=force_preemptive, creds=None, - hostname_override=hostname_override, sanitize_mutual_error_response=sanitize_mutual_error_response) def generate_request_header(self, response, host, is_preemptive=False): @@ -47,6 +48,19 @@ def generate_request_header(self, response, host, is_preemptive=False): name = gssapi.Name(self.principal) self.creds = gssapi.Credentials(name=name, usage="initiate") + # contexts still need to be stored by host, but hostname_override + # allows use of an arbitrary hostname for the GSSAPI exchange (eg, + # in cases of aliased hosts, internal vs external, CNAMEs w/ + # name-based HTTP hosting) + if self.service is not None: + gss_stage = "initiating context" + kerb_host = host + if self.hostname_override: + kerb_host = self.hostname_override + + kerb_spn = "{0}@{1}".format(self.service, kerb_host) + self.target_name = gssapi.Name(kerb_spn) + return HTTPSPNEGOAuth.generate_request_header(self, response, host, is_preemptive) except gssapi.exceptions.GSSError as error: diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index 9942e1e..7e218f1 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -80,17 +80,16 @@ def _negotiate_value(response): class HTTPSPNEGOAuth(AuthBase): """Attaches HTTP GSSAPI Authentication to the given Request object.""" - def __init__(self, mutual_authentication=REQUIRED, service="HTTP", + def __init__(self, mutual_authentication=REQUIRED, target_name="HTTP", delegate=False, opportunistic_auth=False, creds=None, - hostname_override=None, sanitize_mutual_error_response=True): + sanitize_mutual_error_response=True): self.context = {} + self.pos = None self.mutual_authentication = mutual_authentication + self.target_name = target_name self.delegate = delegate - self.pos = None - self.service = service self.opportunistic_auth = opportunistic_auth self.creds = creds - self.hostname_override = hostname_override self.sanitize_mutual_error_response = sanitize_mutual_error_response def generate_request_header(self, response, host, is_preemptive=False): @@ -108,19 +107,14 @@ def generate_request_header(self, response, host, is_preemptive=False): gssflags.append(gssapi.RequirementFlag.delegate_to_peer) try: - # contexts still need to be stored by host, but hostname_override - # allows use of an arbitrary hostname for the GSSAPI exchange - # (eg, in cases of aliased hosts, internal vs external, CNAMEs - # w/ name-based HTTP hosting) - kerb_host = host - if self.hostname_override: - kerb_host = self.hostname_override - - kerb_spn = "{0}@{1}".format(self.service, kerb_host) - gss_stage = "initiating context" + if type(self.target_name) != gssapi.Name: + if '@' not in self.target_name: + self.target_name = "%s@%s" % (self.target_name, host) + + self.target_name = gssapi.Name(self.target_name) self.context[host] = gssapi.SecurityContext( - usage="initiate", flags=gssflags, name=gssapi.Name(kerb_spn), + usage="initiate", flags=gssflags, name=self.target_name, creds=self.creds) gss_stage = "stepping context" diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index 003fae1..bdc5d31 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -589,6 +589,21 @@ def test_explicit_creds(self): usage="initiate", flags=gssflags, creds="fake creds") fake_resp.assert_called_with("token") + def test_target_name(self): + with patch.multiple("gssapi.SecurityContext", __init__=fake_init, + step=fake_resp): + response = requests.Response() + response.url = "http://www.example.org/" + response.headers = {'www-authenticate': 'negotiate token'} + host = urlparse(response.url).hostname + auth = requests_gssapi.HTTPSPNEGOAuth( + target_name="HTTP@otherhost.otherdomain.org") + auth.generate_request_header(response, host) + fake_init.assert_called_with( + name=gssapi.Name("HTTP@otherhost.otherdomain.org"), + usage="initiate", flags=gssflags, creds=None) + fake_resp.assert_called_with("token") + if __name__ == '__main__': unittest.main() From 83b5e5cc4a08619e392094ec8bc4f60935314996 Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Thu, 7 Dec 2017 16:50:14 -0500 Subject: [PATCH 8/8] Update docs for deprecation changes and new features Signed-off-by: Robbie Harwood --- README.rst | 35 +++++++++++++++++++++-------------- requests_gssapi/gssapi_.py | 22 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index b2f255d..47c87d4 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,15 @@ requests GSSAPI authentication library Requests is an HTTP library, written in Python, for human beings. This library adds optional GSSAPI authentication support and supports mutual -authentication. Basic GET usage: +authentication. + +It provides a fully backward-compatible shim for the old +python-requests-kerberos library: simply replace ``import requests_kerberos`` +with ``import requests_gssapi``. A more powerful interface is provided by the +HTTPSPNEGOAuth component, but this is of course not guaranteed to be +compatible. Documentation below is written toward the new interface. + +Basic GET usage: .. code-block:: python @@ -77,8 +85,8 @@ authentication, you can do that as well: >>> r = requests.get("http://example.org", auth=gssapi_auth) ... -Preemptive Authentication -------------------------- +Opportunistic Authentication +---------------------------- ``HTTPSPNEGOAuth`` can be forced to preemptively initiate the GSSAPI exchange and present a token on the initial request (and all @@ -87,13 +95,13 @@ subsequent). By default, authentication only occurs after a is received from the origin server. This can cause mutual authentication failures for hosts that use a persistent connection (eg, Windows/WinRM), as no GSSAPI challenges are sent after the initial auth handshake. This -behavior can be altered by setting ``force_preemptive=True``: +behavior can be altered by setting ``opportunistic_auth=True``: .. code-block:: python >>> import requests >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED - >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=REQUIRED, force_preemptive=True) + >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=REQUIRED, opportunistic_authentication=True) >>> r = requests.get("https://windows.example.org/wsman", auth=gssapi_auth) ... @@ -103,31 +111,30 @@ Hostname Override If communicating with a host whose DNS name doesn't match its hostname (eg, behind a content switch or load balancer), the hostname used for the GSSAPI exchange can be overridden by -setting the ``hostname_override`` arg: +passing in a custom name (string or ``gssapi.Name``): .. code-block:: python >>> import requests >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED - >>> gssapi_auth = HTTPSPNEGOAuth(hostname_override="internalhost.local") + >>> gssapi_auth = HTTPSPNEGOAuth(target_name="internalhost.local") >>> r = requests.get("https://externalhost.example.org/", auth=gssapi_auth) ... Explicit Principal ------------------ -``HTTPSPNEGOAuth`` 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 GSSAPI to look for a matching credential cache for the named user. -This feature depends on OS support for collection-type credential caches. -An explicit principal can be specified with the ``principal`` arg: +``HTTPSPNEGOAuth`` 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 credential can be in instead, if desired. .. code-block:: python + >>> import gssapi >>> import requests >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED - >>> gssapi_auth = HTTPSPNEGOAuth(principal="user@REALM") + >>> creds = gssapi.Credentials(name=gssapi.Name("user@REALM"), usage="initiate") + >>> gssapi_auth = HTTPSPNEGOAuth(creds=creds) >>> r = requests.get("http://example.org", auth=gssapi_auth) ... diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index 7e218f1..1a9bc04 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -79,7 +79,27 @@ def _negotiate_value(response): class HTTPSPNEGOAuth(AuthBase): - """Attaches HTTP GSSAPI Authentication to the given Request object.""" + """Attaches HTTP GSSAPI Authentication to the given Request object. + + `mutual_authentication` controls whether GSSAPI should attempt mutual + authentication. It may be `REQUIRED` (default), `OPTIONAL`, or + `DISABLED`. + + `target_name` specifies the remote principal name. It may be either a + GSSAPI name type or a string (default: "HTTP" at the DNS host). + + `delegate` indicates whether we should attempt credential delegation. + Default is `False`. + + `opportunistic_auth` indicates whether we should assume the server will + ask for Negotiation. Defaut is `False`. + + `creds` is GSSAPI credentials (gssapi.Credentials) to use for negotiation. + Default is `None`. + + `sanitize_mutual_error_response` controls whether we should clean up + server responses. See the `SanitizedResponse` class. + """ def __init__(self, mutual_authentication=REQUIRED, target_name="HTTP", delegate=False, opportunistic_auth=False, creds=None, sanitize_mutual_error_response=True):