diff --git a/HISTORY.rst b/HISTORY.rst index e9d1e2d..e30c626 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,14 @@ History ======= +1.0.0: 2017-12-14 +----------------- + +- Fork project to requests-gssapi +- Replace pykerberos with python-gssapi +- Add HTTPSPNEGOAuth interface. HTTPKerberosAuth is retained as a shim, but + bump the major version anyway for clarity. + 0.11.0: 2016-11-02 ------------------ diff --git a/README.rst b/README.rst index 47c87d4..0d3e37d 100644 --- a/README.rst +++ b/README.rst @@ -133,7 +133,8 @@ applicable). However, an explicit credential can be in instead, if desired. >>> import gssapi >>> import requests >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED - >>> creds = gssapi.Credentials(name=gssapi.Name("user@REALM"), usage="initiate") + >>> name = gssapi.Name("user@REALM", gssapi.NameType.hostbased_service) + >>> creds = gssapi.Credentials(name=name, usage="initiate") >>> gssapi_auth = HTTPSPNEGOAuth(creds=creds) >>> r = requests.get("http://example.org", auth=gssapi_auth) ... diff --git a/requests_gssapi/__init__.py b/requests_gssapi/__init__.py index 4c69401..b1839af 100644 --- a/requests_gssapi/__init__.py +++ b/requests_gssapi/__init__.py @@ -22,4 +22,4 @@ __all__ = ('HTTPSPNEGOAuth', 'HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', 'OPTIONAL', 'DISABLED') -__version__ = '0.11.0' +__version__ = '1.0.0' diff --git a/requests_gssapi/compat.py b/requests_gssapi/compat.py index ed18142..99eb7b7 100644 --- a/requests_gssapi/compat.py +++ b/requests_gssapi/compat.py @@ -45,7 +45,8 @@ def generate_request_header(self, response, host, is_preemptive=False): try: if self.principal is not None: gss_stage = "acquiring credentials" - name = gssapi.Name(self.principal) + name = gssapi.Name( + self.principal, gssapi.NameType.hostbased_service) self.creds = gssapi.Credentials(name=name, usage="initiate") # contexts still need to be stored by host, but hostname_override @@ -59,7 +60,8 @@ def generate_request_header(self, response, host, is_preemptive=False): kerb_host = self.hostname_override kerb_spn = "{0}@{1}".format(self.service, kerb_host) - self.target_name = gssapi.Name(kerb_spn) + self.target_name = gssapi.Name( + kerb_spn, gssapi.NameType.hostbased_service) return HTTPSPNEGOAuth.generate_request_header(self, response, host, is_preemptive) diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index 1a9bc04..8457f30 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -1,6 +1,8 @@ import re import logging +from base64 import b64encode, b64decode + import gssapi from requests.auth import AuthBase @@ -73,7 +75,7 @@ def _negotiate_value(response): if authreq: match_obj = regex.search(authreq) if match_obj: - return match_obj.group(1) + return b64decode(match_obj.group(1)) return None @@ -132,7 +134,8 @@ def generate_request_header(self, response, host, is_preemptive=False): if '@' not in self.target_name: self.target_name = "%s@%s" % (self.target_name, host) - self.target_name = gssapi.Name(self.target_name) + self.target_name = gssapi.Name( + self.target_name, gssapi.NameType.hostbased_service) self.context[host] = gssapi.SecurityContext( usage="initiate", flags=gssflags, name=self.target_name, creds=self.creds) @@ -144,7 +147,7 @@ def generate_request_header(self, response, host, is_preemptive=False): gss_response = self.context[host].step( _negotiate_value(response)) - return "Negotiate {0}".format(gss_response) + return "Negotiate {0}".format(b64encode(gss_response).decode()) except gssapi.exceptions.GSSError as error: msg = error.gen_message() diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index bdc5d31..cd4adcd 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -3,6 +3,7 @@ """Tests for requests_gssapi.""" +from base64 import b64encode from mock import Mock, patch from requests.compat import urlparse import requests @@ -18,8 +19,8 @@ # > -- sigmavirus24 in https://github.com/requests/requests-kerberos/issues/1 fake_init = Mock(return_value=None) -fake_creds = Mock(return_value="fake creds") -fake_resp = Mock(return_value="GSSRESPONSE") +fake_creds = Mock(return_value=b"fake creds") +fake_resp = Mock(return_value=b"GSSRESPONSE") # GSSAPI exceptions require a major and minor status code for their # construction, so construct a *really* fake one @@ -29,6 +30,16 @@ gssapi.RequirementFlag.out_of_sequence_detection] gssdelegflags = gssflags + [gssapi.RequirementFlag.delegate_to_peer] +# The base64 behavior we want is that encoding produces a string, but decoding +# produces bytes. Remember, GSSAPI tokens are opaque here. +b64_negotiate_response = "Negotiate " + b64encode(b"GSSRESPONSE").decode() +b64_negotiate_token = "negotiate " + b64encode(b"token").decode() +b64_negotiate_server = "negotiate " + b64encode(b"servertoken").decode() + + +def gssapi_name(s): + return gssapi.Name(s, gssapi.NameType.hostbased_service) + class GSSAPITestCase(unittest.TestCase): def setUp(self): @@ -44,10 +55,10 @@ def tearDown(self): def test_negotate_value_extraction(self): response = requests.Response() - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} self.assertEqual( requests_gssapi.gssapi_._negotiate_value(response), - 'token' + b'token' ) def test_negotate_value_extraction_none(self): @@ -67,7 +78,7 @@ def test_force_preemptive(self): self.assertTrue('Authorization' in request.headers) self.assertEqual(request.headers.get('Authorization'), - 'Negotiate GSSRESPONSE') + b64_negotiate_response) def test_no_force_preemptive(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -85,29 +96,29 @@ def test_generate_request_header(self): step=fake_resp): response = requests.Response() response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} host = urlparse(response.url).hostname auth = requests_gssapi.HTTPKerberosAuth() self.assertEqual( auth.generate_request_header(response, host), - "Negotiate GSSRESPONSE") + b64_negotiate_response) fake_init.assert_called_with( - name=gssapi.Name("HTTP@www.example.org"), + name=gssapi_name("HTTP@www.example.org"), creds=None, flags=gssflags, usage="initiate") - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") def test_generate_request_header_init_error(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, step=fail_resp): response = requests.Response() response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} host = urlparse(response.url).hostname auth = requests_gssapi.HTTPKerberosAuth() self.assertRaises(requests_gssapi.exceptions.SPNEGOExchangeError, auth.generate_request_header, response, host) fake_init.assert_called_with( - name=gssapi.Name("HTTP@www.example.org"), + name=gssapi_name("HTTP@www.example.org"), usage="initiate", flags=gssflags, creds=None) def test_generate_request_header_step_error(self): @@ -115,15 +126,15 @@ def test_generate_request_header_step_error(self): step=fail_resp): response = requests.Response() response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} host = urlparse(response.url).hostname auth = requests_gssapi.HTTPKerberosAuth() self.assertRaises(requests_gssapi.exceptions.SPNEGOExchangeError, auth.generate_request_header, response, host) fake_init.assert_called_with( - name=gssapi.Name("HTTP@www.example.org"), + name=gssapi_name("HTTP@www.example.org"), usage="initiate", flags=gssflags, creds=None) - fail_resp.assert_called_with("token") + fail_resp.assert_called_with(b"token") def test_authenticate_user(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -131,7 +142,7 @@ def test_authenticate_user(self): response_ok = requests.Response() response_ok.url = "http://www.example.org/" response_ok.status_code = 200 - response_ok.headers = {'www-authenticate': 'negotiate servertoken'} + response_ok.headers = {'www-authenticate': b64_negotiate_server} connection = Mock() connection.send = Mock(return_value=response_ok) @@ -143,7 +154,7 @@ def test_authenticate_user(self): response = requests.Response() response.request = request response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} response.status_code = 401 response.connection = connection response._content = "" @@ -154,13 +165,13 @@ def test_authenticate_user(self): self.assertTrue(response in r.history) self.assertEqual(r, response_ok) self.assertEqual(request.headers['Authorization'], - 'Negotiate GSSRESPONSE') + b64_negotiate_response) connection.send.assert_called_with(request) raw.release_conn.assert_called_with() fake_init.assert_called_with( - name=gssapi.Name("HTTP@www.example.org"), + name=gssapi_name("HTTP@www.example.org"), flags=gssflags, usage="initiate", creds=None) - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") def test_handle_401(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -168,7 +179,7 @@ def test_handle_401(self): response_ok = requests.Response() response_ok.url = "http://www.example.org/" response_ok.status_code = 200 - response_ok.headers = {'www-authenticate': 'negotiate servertoken'} + response_ok.headers = {'www-authenticate': b64_negotiate_server} connection = Mock() connection.send = Mock(return_value=response_ok) @@ -180,7 +191,7 @@ def test_handle_401(self): response = requests.Response() response.request = request response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} response.status_code = 401 response.connection = connection response._content = "" @@ -191,13 +202,13 @@ def test_handle_401(self): self.assertTrue(response in r.history) self.assertEqual(r, response_ok) self.assertEqual(request.headers['Authorization'], - 'Negotiate GSSRESPONSE') + b64_negotiate_response) connection.send.assert_called_with(request) raw.release_conn.assert_called_with() fake_init.assert_called_with( - name=gssapi.Name("HTTP@www.example.org"), + name=gssapi_name("HTTP@www.example.org"), creds=None, flags=gssflags, usage="initiate") - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") def test_authenticate_server(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -206,15 +217,15 @@ def test_authenticate_server(self): response_ok.url = "http://www.example.org/" response_ok.status_code = 200 response_ok.headers = { - 'www-authenticate': 'negotiate servertoken', - 'authorization': 'Negotiate GSSRESPONSE'} + 'www-authenticate': b64_negotiate_server, + 'authorization': b64_negotiate_response} auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": gssapi.SecurityContext} result = auth.authenticate_server(response_ok) self.assertTrue(result) - fake_resp.assert_called_with("servertoken") + fake_resp.assert_called_with(b"servertoken") def test_handle_other(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -223,8 +234,8 @@ def test_handle_other(self): response_ok.url = "http://www.example.org/" response_ok.status_code = 200 response_ok.headers = { - 'www-authenticate': 'negotiate servertoken', - 'authorization': 'Negotiate GSSRESPONSE'} + 'www-authenticate': b64_negotiate_server, + 'authorization': b64_negotiate_response} auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": gssapi.SecurityContext} @@ -232,7 +243,7 @@ def test_handle_other(self): r = auth.handle_other(response_ok) self.assertEqual(r, response_ok) - fake_resp.assert_called_with("servertoken") + fake_resp.assert_called_with(b"servertoken") def test_handle_response_200(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -241,8 +252,8 @@ def test_handle_response_200(self): response_ok.url = "http://www.example.org/" response_ok.status_code = 200 response_ok.headers = { - 'www-authenticate': 'negotiate servertoken', - 'authorization': 'Negotiate GSSRESPONSE'} + 'www-authenticate': b64_negotiate_server, + 'authorization': b64_negotiate_response} auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": gssapi.SecurityContext} @@ -250,7 +261,7 @@ def test_handle_response_200(self): r = auth.handle_response(response_ok) self.assertEqual(r, response_ok) - fake_resp.assert_called_with("servertoken") + fake_resp.assert_called_with(b"servertoken") def test_handle_response_200_mutual_auth_required_failure(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -276,8 +287,8 @@ def test_handle_response_200_mutual_auth_required_failure_2(self): response_ok.url = "http://www.example.org/" response_ok.status_code = 200 response_ok.headers = { - 'www-authenticate': 'negotiate servertoken', - 'authorization': 'Negotiate GSSRESPONSE'} + 'www-authenticate': b64_negotiate_server, + 'authorization': b64_negotiate_response} auth = requests_gssapi.HTTPKerberosAuth() auth.context = {"www.example.org": gssapi.SecurityContext} @@ -285,7 +296,7 @@ def test_handle_response_200_mutual_auth_required_failure_2(self): self.assertRaises(requests_gssapi.MutualAuthenticationError, auth.handle_response, response_ok) - fail_resp.assert_called_with("servertoken") + fail_resp.assert_called_with(b"servertoken") def test_handle_response_200_mutual_auth_optional_hard_failure(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -294,8 +305,8 @@ def test_handle_response_200_mutual_auth_optional_hard_failure(self): response_ok.url = "http://www.example.org/" response_ok.status_code = 200 response_ok.headers = { - 'www-authenticate': 'negotiate servertoken', - 'authorization': 'Negotiate GSSRESPONSE'} + 'www-authenticate': b64_negotiate_server, + 'authorization': b64_negotiate_response} auth = requests_gssapi.HTTPKerberosAuth( requests_gssapi.OPTIONAL) @@ -304,7 +315,7 @@ def test_handle_response_200_mutual_auth_optional_hard_failure(self): self.assertRaises(requests_gssapi.MutualAuthenticationError, auth.handle_response, response_ok) - fail_resp.assert_called_with("servertoken") + fail_resp.assert_called_with(b"servertoken") def test_handle_response_200_mutual_auth_optional_soft_failure(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -398,7 +409,7 @@ def test_handle_response_401(self): response_ok = requests.Response() response_ok.url = "http://www.example.org/" response_ok.status_code = 200 - response_ok.headers = {'www-authenticate': 'negotiate servertoken'} + response_ok.headers = {'www-authenticate': b64_negotiate_server} connection = Mock() connection.send = Mock(return_value=response_ok) @@ -410,7 +421,7 @@ def test_handle_response_401(self): response = requests.Response() response.request = request response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} response.status_code = 401 response.connection = connection response._content = "" @@ -425,13 +436,13 @@ def test_handle_response_401(self): auth.handle_other.assert_called_once_with(response_ok) self.assertEqual(r, response_ok) self.assertEqual(request.headers['Authorization'], - 'Negotiate GSSRESPONSE') + b64_negotiate_response) connection.send.assert_called_with(request) raw.release_conn.assert_called_with() fake_init.assert_called_with( - name=gssapi.Name("HTTP@www.example.org"), + name=gssapi_name("HTTP@www.example.org"), usage="initiate", flags=gssflags, creds=None) - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") def test_handle_response_401_rejected(self): # Get a 401 from server, authenticate, and get another 401 back. @@ -456,7 +467,7 @@ def connection_send(self, *args, **kwargs): response = requests.Response() response.request = request response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} response.status_code = 401 response.connection = connection response._content = "" @@ -468,27 +479,27 @@ def connection_send(self, *args, **kwargs): self.assertEqual(r.status_code, 401) self.assertEqual(request.headers['Authorization'], - 'Negotiate GSSRESPONSE') + b64_negotiate_response) connection.send.assert_called_with(request) raw.release_conn.assert_called_with() fake_init.assert_called_with( - name=gssapi.Name("HTTP@www.example.org"), + name=gssapi_name("HTTP@www.example.org"), usage="initiate", flags=gssflags, creds=None) - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") def test_generate_request_header_custom_service(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'} + response.headers = {'www-authenticate': b64_negotiate_token} host = urlparse(response.url).hostname auth = requests_gssapi.HTTPKerberosAuth(service="barfoo") auth.generate_request_header(response, host), fake_init.assert_called_with( - name=gssapi.Name("barfoo@www.example.org"), + name=gssapi_name("barfoo@www.example.org"), usage="initiate", flags=gssflags, creds=None) - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") def test_delegation(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -496,7 +507,7 @@ def test_delegation(self): response_ok = requests.Response() response_ok.url = "http://www.example.org/" response_ok.status_code = 200 - response_ok.headers = {'www-authenticate': 'negotiate servertoken'} + response_ok.headers = {'www-authenticate': b64_negotiate_server} connection = Mock() connection.send = Mock(return_value=response_ok) @@ -508,7 +519,7 @@ def test_delegation(self): response = requests.Response() response.request = request response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} response.status_code = 401 response.connection = connection response._content = "" @@ -519,13 +530,13 @@ def test_delegation(self): self.assertTrue(response in r.history) self.assertEqual(r, response_ok) self.assertEqual(request.headers['Authorization'], - 'Negotiate GSSRESPONSE') + b64_negotiate_response) connection.send.assert_called_with(request) raw.release_conn.assert_called_with() fake_init.assert_called_with( - name=gssapi.Name("HTTP@www.example.org"), + name=gssapi_name("HTTP@www.example.org"), usage="initiate", flags=gssdelegflags, creds=None) - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") def test_principal_override(self): with patch.multiple("gssapi.Credentials", __new__=fake_creds), \ @@ -533,32 +544,31 @@ def test_principal_override(self): step=fake_resp): response = requests.Response() response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_negotiate_token} host = urlparse(response.url).hostname auth = requests_gssapi.HTTPKerberosAuth(principal="user@REALM") auth.generate_request_header(response, host) fake_creds.assert_called_with(gssapi.creds.Credentials, usage="initiate", - name=gssapi.Name("user@REALM")) + name=gssapi_name("user@REALM")) 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") + name=gssapi_name("HTTP@www.example.org"), + usage="initiate", flags=gssflags, creds=b"fake creds") def test_realm_override(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'} + response.headers = {'www-authenticate': b64_negotiate_token} host = urlparse(response.url).hostname auth = requests_gssapi.HTTPKerberosAuth( hostname_override="otherhost.otherdomain.org") auth.generate_request_header(response, host) fake_init.assert_called_with( - name=gssapi.Name("HTTP@otherhost.otherdomain.org"), + name=gssapi_name("HTTP@otherhost.otherdomain.org"), usage="initiate", flags=gssflags, creds=None) - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") def test_opportunistic_auth(self): with patch.multiple("gssapi.SecurityContext", __init__=fake_init, @@ -571,7 +581,7 @@ def test_opportunistic_auth(self): self.assertTrue('Authorization' in request.headers) self.assertEqual(request.headers.get('Authorization'), - 'Negotiate GSSRESPONSE') + b64_negotiate_response) def test_explicit_creds(self): with patch.multiple("gssapi.Credentials", __new__=fake_creds), \ @@ -579,30 +589,30 @@ def test_explicit_creds(self): step=fake_resp): response = requests.Response() response.url = "http://www.example.org/" - response.headers = {'www-authenticate': 'negotiate token'} + response.headers = {'www-authenticate': b64_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") + name=gssapi_name("HTTP@www.example.org"), + usage="initiate", flags=gssflags, creds=b"fake creds") + fake_resp.assert_called_with(b"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'} + response.headers = {'www-authenticate': b64_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"), + name=gssapi_name("HTTP@otherhost.otherdomain.org"), usage="initiate", flags=gssflags, creds=None) - fake_resp.assert_called_with("token") + fake_resp.assert_called_with(b"token") if __name__ == '__main__':