diff --git a/doc/source/authentication-plugins.rst b/doc/source/authentication-plugins.rst index cf25be7..975d958 100644 --- a/doc/source/authentication-plugins.rst +++ b/doc/source/authentication-plugins.rst @@ -55,6 +55,8 @@ this V3 defines a number of different against a V3 identity service using a username and password. - :py:class:`~keystoneauth1.identity.v3.TokenMethod`: Authenticate against a V3 identity service using an existing token. +- :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`: Authenticate + against a V3 identity service using Kerberos. The :py:class:`~keystoneauth1.identity.v3.AuthMethod` objects are then passed to the :py:class:`~keystoneauth1.identity.v3.Auth` plugin:: @@ -78,6 +80,8 @@ like the V2 plugins: only a :py:class:`~keystoneauth1.identity.v3.PasswordMethod`. - :py:class:`~keystoneauth1.identity.v3.Token`: Authenticate using only a :py:class:`~keystoneauth1.identity.v3.TokenMethod`. +- :py:class:`~keystoneauth1.extras.kerberos.Kerberos`: Authenticate using + only a :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`. :: diff --git a/keystoneauth1/extras/kerberos.py b/keystoneauth1/extras/kerberos.py new file mode 100644 index 0000000..5a4c4f4 --- /dev/null +++ b/keystoneauth1/extras/kerberos.py @@ -0,0 +1,67 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +"""Kerberos authentication plugins. + +.. warning:: + This module requires installation of an extra package (`requests_kerberos`) + not installed by default. Without the extra package an import error will + occur. The extra package can be installed using:: + + $ pip install keystoneauth['kerberos'] + +""" + +import requests_kerberos + +from keystoneauth1 import access +from keystoneauth1.identity import v3 +from keystoneauth1.identity.v3 import federation + + +def _requests_auth(): + # NOTE(jamielennox): request_kerberos.OPTIONAL allows the plugin to accept + # unencrypted error messages where we can't verify the origin of the error + # because we aren't authenticated. + return requests_kerberos.HTTPKerberosAuth( + mutual_authentication=requests_kerberos.OPTIONAL) + + +class KerberosMethod(v3.AuthMethod): + + _method_parameters = [] + + def get_auth_data(self, session, auth, headers, request_kwargs, **kwargs): + # NOTE(jamielennox): request_kwargs is passed as a kwarg however it is + # required and always present when called from keystoneclient. + request_kwargs['requests_auth'] = _requests_auth() + return 'kerberos', {} + + +class Kerberos(v3.AuthConstructor): + _auth_method_class = KerberosMethod + + +class MappedKerberos(federation.FederationBaseAuth): + """Authenticate using Kerberos via the keystone federation mechanisms. + + This uses the OS-FEDERATION extension to gain an unscoped token and then + use the standard keystone auth process to scope that to any given project. + """ + + def get_unscoped_auth_ref(self, session, **kwargs): + resp = session.get(self.federated_token_url, + requests_auth=_requests_auth(), + authenticated=False) + + return access.create(body=resp.json(), resp=resp) diff --git a/keystoneauth1/tests/unit/extras/kerberos/__init__.py b/keystoneauth1/tests/unit/extras/kerberos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keystoneauth1/tests/unit/extras/kerberos/base.py b/keystoneauth1/tests/unit/extras/kerberos/base.py new file mode 100644 index 0000000..f4594af --- /dev/null +++ b/keystoneauth1/tests/unit/extras/kerberos/base.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneauth1.tests.unit.extras.kerberos import utils +from keystoneauth1.tests.unit import utils as test_utils + + +REQUEST = {'auth': {'identity': {'methods': ['kerberos'], + 'kerberos': {}}}} + + +class TestCase(test_utils.TestCase): + + """Test case base class for Kerberos unit tests.""" + + TEST_V3_URL = test_utils.TestCase.TEST_ROOT_URL + 'v3' + + def setUp(self): + super(TestCase, self).setUp() + + km = utils.KerberosMock(self.requests_mock) + self.kerberos_mock = self.useFixture(km) + + def assertRequestBody(self, body=None): + """Ensure the request body is the standard Kerberos auth request. + + :param dict body: the body to compare. If not provided the last request + body will be used. + """ + if not body: + body = self.requests_mock.last_request.json() + + self.assertEqual(REQUEST, body) diff --git a/keystoneauth1/tests/unit/extras/kerberos/test_mapped.py b/keystoneauth1/tests/unit/extras/kerberos/test_mapped.py new file mode 100644 index 0000000..9ba7fbb --- /dev/null +++ b/keystoneauth1/tests/unit/extras/kerberos/test_mapped.py @@ -0,0 +1,90 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +import six + +from keystoneauth1 import fixture as ks_fixture +from keystoneauth1 import session +from keystoneauth1.tests.unit.extras.kerberos import base + +try: + # Until requests_kerberos gets py3 support, this is going to fail to import + from keystoneauth1.extras import kerberos + +except ImportError: + if six.PY2: + # requests_kerberos is expected to be there on py2, so don't ignore. + raise + + # requests_kerberos isn't available + kerberos = False + + +class TestMappedAuth(base.TestCase): + + def setUp(self): + if not kerberos: + self.skipTest("Kerberos support isn't available.") + + super(TestMappedAuth, self).setUp() + + self.protocol = uuid.uuid4().hex + self.identity_provider = uuid.uuid4().hex + + @property + def token_url(self): + fmt = '%s/OS-FEDERATION/identity_providers/%s/protocols/%s/auth' + return fmt % ( + self.TEST_V3_URL, + self.identity_provider, + self.protocol) + + def test_unscoped_mapped_auth(self): + token_id, _ = self.kerberos_mock.mock_auth_success( + url=self.token_url, method='GET') + + plugin = kerberos.MappedKerberos( + auth_url=self.TEST_V3_URL, protocol=self.protocol, + identity_provider=self.identity_provider) + + sess = session.Session() + tok = plugin.get_token(sess) + + self.assertEqual(token_id, tok) + + def test_project_scoped_mapped_auth(self): + self.kerberos_mock.mock_auth_success(url=self.token_url, + method='GET') + + scoped_id = uuid.uuid4().hex + scoped_body = ks_fixture.V3Token() + scoped_body.set_project_scope() + + self.requests_mock.post( + '%s/auth/tokens' % self.TEST_V3_URL, + json=scoped_body, + headers={'X-Subject-Token': scoped_id, + 'Content-Type': 'application/json'}) + + plugin = kerberos.MappedKerberos( + auth_url=self.TEST_V3_URL, protocol=self.protocol, + identity_provider=self.identity_provider, + project_id=scoped_body.project_id) + + sess = session.Session() + tok = plugin.get_token(sess) + proj = plugin.get_project_id(sess) + + self.assertEqual(scoped_id, tok) + self.assertEqual(scoped_body.project_id, proj) diff --git a/keystoneauth1/tests/unit/extras/kerberos/test_v3.py b/keystoneauth1/tests/unit/extras/kerberos/test_v3.py new file mode 100644 index 0000000..40df22f --- /dev/null +++ b/keystoneauth1/tests/unit/extras/kerberos/test_v3.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import six + +from keystoneauth1 import session +from keystoneauth1.tests.unit.extras.kerberos import base + +try: + # Until requests_kerberos gets py3 support, this is going to fail to import + from keystoneauth1.extras import kerberos + +except ImportError: + if six.PY2: + # requests_kerberos is expected to be there on py2, so don't ignore. + raise + + # requests_kerberos isn't available + kerberos = None + + +class TestKerberosAuth(base.TestCase): + + def setUp(self): + if not kerberos: + self.skipTest("Kerberos support isn't available.") + + super(TestKerberosAuth, self).setUp() + + def test_authenticate_with_kerberos_domain_scoped(self): + token_id, token_body = self.kerberos_mock.mock_auth_success() + + a = kerberos.Kerberos(self.TEST_ROOT_URL + 'v3') + s = session.Session(a) + token = a.get_token(s) + + self.assertRequestBody() + self.assertEqual( + self.kerberos_mock.challenge_header, + self.requests_mock.last_request.headers['Authorization']) + self.assertEqual(token_id, a.auth_ref.auth_token) + self.assertEqual(token_id, token) diff --git a/keystoneauth1/tests/unit/extras/kerberos/utils.py b/keystoneauth1/tests/unit/extras/kerberos/utils.py new file mode 100644 index 0000000..8c35292 --- /dev/null +++ b/keystoneauth1/tests/unit/extras/kerberos/utils.py @@ -0,0 +1,81 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +import fixtures +from oslotest import mockpatch +try: + # requests_kerberos won't be available on py3, it doesn't work with py3. + import requests_kerberos +except ImportError: + requests_kerberos = None + +from keystoneauth1 import fixture as ks_fixture +from keystoneauth1.tests.unit import utils as test_utils + + +class KerberosMock(fixtures.Fixture): + + def __init__(self, requests_mock): + super(KerberosMock, self).__init__() + + self.challenge_header = 'Negotiate %s' % uuid.uuid4().hex + self.pass_header = 'Negotiate %s' % uuid.uuid4().hex + self.requests_mock = requests_mock + + def setUp(self): + super(KerberosMock, self).setUp() + + m = mockpatch.PatchObject(requests_kerberos.HTTPKerberosAuth, + 'generate_request_header', + self._generate_request_header) + + self.header_fixture = self.useFixture(m) + + m = mockpatch.PatchObject(requests_kerberos.HTTPKerberosAuth, + 'authenticate_server', + self._authenticate_server) + + self.authenticate_fixture = self.useFixture(m) + + def _generate_request_header(self, *args, **kwargs): + return self.challenge_header + + def _authenticate_server(self, response): + return response.headers.get('www-authenticate') == self.pass_header + + def mock_auth_success( + self, + token_id=None, + token_body=None, + method='POST', + url=test_utils.TestCase.TEST_ROOT_URL + 'v3/auth/tokens'): + if not token_id: + token_id = uuid.uuid4().hex + if not token_body: + token_body = ks_fixture.V3Token() + + response_list = [{'text': 'Fail', + 'status_code': 401, + 'headers': {'WWW-Authenticate': 'Negotiate'}}, + {'headers': {'X-Subject-Token': token_id, + 'Content-Type': 'application/json', + 'WWW-Authenticate': self.pass_header}, + 'status_code': 200, + 'json': token_body}] + + self.requests_mock.register_uri(method, + url, + response_list=response_list) + + return token_id, token_body diff --git a/setup.cfg b/setup.cfg index 76adbce..7d5c9d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,9 @@ packages = keystoneauth1 [extras] +kerberos = + requests-kerberos>=0.6:python_version=='2.7' or python_version=='2.6' # MIT + saml2 = lxml>=2.3 diff --git a/tox.ini b/tox.ini index 14e860b..0a97b0a 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt - .[saml2] + .[kerberos,saml2] commands = ostestr {posargs} [testenv:pep8]