From e4a4a98496a3550abee05b77b958ac5a34579472 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Wed, 23 Nov 2016 15:16:28 -0600 Subject: [PATCH] Add functions for authenticating to Craton This adds a cratonclient.auth with craton_auth and keystone_auth functions to generate cratonclient.session.Session objects with appropriate authentication plugins set-up. Closes-bug: 1643961 Change-Id: I661a91241b96ca5c45a91a0add4f74c4ca7e6750 --- cratonclient/auth.py | 198 ++++++++++++++++++++ cratonclient/session.py | 50 +---- cratonclient/tests/integration/test_auth.py | 59 ++++++ cratonclient/tests/unit/test_auth.py | 197 +++++++++++++++++++ cratonclient/tests/unit/test_session.py | 27 +-- doc/source/authentication-documentation.rst | 7 + doc/source/index.rst | 6 +- doc/source/usage.rst | 11 +- 8 files changed, 489 insertions(+), 66 deletions(-) create mode 100644 cratonclient/auth.py create mode 100644 cratonclient/tests/integration/test_auth.py create mode 100644 cratonclient/tests/unit/test_auth.py create mode 100644 doc/source/authentication-documentation.rst diff --git a/cratonclient/auth.py b/cratonclient/auth.py new file mode 100644 index 0000000..1f7b5b6 --- /dev/null +++ b/cratonclient/auth.py @@ -0,0 +1,198 @@ +# Copyright (c) 2016 Rackspace +# +# 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. +"""Module that simplifies and unifies authentication for Craton.""" +from keystoneauth1.identity.v3 import password as ksa_password +from keystoneauth1 import plugin +from keystoneauth1 import session as ksa_session + +from cratonclient import exceptions as exc + + +def craton_auth(username, token, project_id, verify=True): + """Configure a cratonclient Session to authenticate to Craton. + + This will create, configure, and return a Session object that will use + Craton's built-in authentication method. + + :param str username: + The username with which to authentiate against the API. + :param str token: + The token with which to authenticate against the API. + :param str project_id: + The project ID that the user belongs to. + :param bool verify: + (Optional) Whether or not to verify HTTPS certificates provided by the + server. Default: True + :returns: + Configured cratonclient session. + :rtype: + cratonclient.session.Session + + Example: + + .. code-block:: python + + from cratonclient import auth + from cratonclient.v1 import client + + craton = client.Client(session=auth.craton_auth( + username='demo', + token='demo', + project_id='b9f10eca66ac4c279c139d01e65f96b4', + )) + + """ + auth_plugin = CratonAuth( + username=username, + token=token, + project_id=project_id, + ) + return create_session_with(auth_plugin, verify) + + +def keystone_auth(auth_url, username, password, verify=True, + project_name=None, project_id=None, + project_domain_name=None, project_domain_id=None, + user_domain_name=None, user_domain_id=None, + **auth_parameters): + r"""Configure a cratonclient Session to authenticate with Keystone. + + This will create, configure, and return a Session using thet appropriate + Keystone authentication plugin to be able to communicate and authenticate + to Craton. + + .. note:: + + Presently, this function supports only V3 Password based + authentication to Keystone. We also do not validate that you specify + required attributes. For example, Keystone will require you provide + ``project_name`` or ``project_id`` but we will not enforce whether or + not you've specified one. + + :param str auth_url: + The URL of the Keystone instance to authenticate to. + :param str username: + The username with which we will authenticate to Keystone. + :param str password: + The password used to authenticate to Keystone. + :param str project_name: + (Optional) The name of the project the user belongs to. + :param str project_id: + (Optional) The ID of the project the user belongs to. + :param str project_domain_name: + (Optional) The name of the project's domain. + :param str project_domain_id: + (Optional) The ID of the project's domain. + :param str user_domain_name: + (Optional) The name of the user's domain. + :param str user_domain_id: + (Optional) The ID of the user's domain. + :param bool verify: + (Optional) Whether or not to verify HTTPS certificates provided by the + server. Default: True + :param \*\*auth_parameters: + Any extra authentication parameters used to authenticate to Keystone. + See the Keystone documentation for usage of: + - ``trust_id`` + - ``domain_id`` + - ``domain_name`` + - ``reauthenticate`` + :returns: + Configured cratonclient session. + :rtype: + cratonclient.session.Session + + Example: + + .. code-block:: python + + from cratonclient import auth + from cratonclient.v1 import client + + craton = client.Client(session=auth.keystone_auth( + auth_url='https://keystone.cloud.org/v3', + username='admin', + password='s3cr373p@55w0rd', + project_name='admin', + project_domain_name='Default', + user_domain_name='Default', + )) + """ + password_auth = ksa_password.Password( + auth_url=auth_url, + username=username, + password=password, + project_id=project_id, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + **auth_parameters + ) + return create_session_with(password_auth, verify) + + +def create_session_with(auth_plugin, verify): + """Create a cratonclient Session with the specified auth and verify values. + + :param auth_plugin: + The authentication plugin to use with the keystoneauth1 Session + object. + :type auth_plugin: + keystoneauth1.plugin.BaseAuthPlugin + :param bool verify: + Whether or not to verify HTTPS certificates provided by the server. + :returns: + Configured cratonclient session. + :rtype: + cratonclient.session.Session + """ + from cratonclient import session + return session.Session(session=ksa_session.Session( + auth=auth_plugin, + verify=verify, + )) + + +class CratonAuth(plugin.BaseAuthPlugin): + """Custom authentication plugin for keystoneauth1. + + This is specifically for the case where we're not using Keystone for + authentication. + """ + + def __init__(self, username, project_id, token): + """Initialize our craton authentication class.""" + self.username = username + self.project_id = project_id + self.token = token + + def get_token(self, session, **kwargs): + """Return our token.""" + return self.token + + def get_headers(self, session, **kwargs): + """Return the craton authentication headers.""" + headers = super(CratonAuth, self).get_headers(session, **kwargs) + if headers is None: + # NOTE(sigmavirus24): This means that the token must be None. We + # should not allow this to go further. We're using built-in Craton + # authentication (not authenticating against Keystone) so we will + # be unable to authenticate. + raise exc.UnableToAuthenticate() + + headers['X-Auth-User'] = self.username + headers['X-Auth-Project'] = '{}'.format(self.project_id) + return headers diff --git a/cratonclient/session.py b/cratonclient/session.py index 3785a70..41e2958 100644 --- a/cratonclient/session.py +++ b/cratonclient/session.py @@ -14,11 +14,11 @@ """Craton-specific session details.""" import logging -from keystoneauth1 import plugin from keystoneauth1 import session as ksa_session from requests import exceptions as requests_exc import cratonclient +from cratonclient import auth from cratonclient import exceptions as exc LOG = logging.getLogger(__name__) @@ -46,16 +46,16 @@ class Session(object): :param str project_id: The user's project id in Craton. """ - self._auth = None if session is None: - self._auth = CratonAuth(username=username, - project_id=project_id, - token=token) - craton_user_agent = 'python-cratonclient/{0}'.format( - cratonclient.__version__) - session = ksa_session.Session(auth=self._auth, - user_agent=craton_user_agent) + _auth = auth.CratonAuth( + username=username, + project_id=project_id, + token=token, + ) + session = ksa_session.Session(auth=_auth) self._session = session + self._session.user_agent = 'python-cratonclient/{0}'.format( + cratonclient.__version__) def delete(self, url, **kwargs): """Make a DELETE request with url and optional parameters. @@ -232,35 +232,3 @@ class Session(object): raise exc.error_from(response) return response - - -class CratonAuth(plugin.BaseAuthPlugin): - """Custom authentication plugin for keystoneauth1. - - This is specifically for the case where we're not using Keystone for - authentication. - """ - - def __init__(self, username, project_id, token): - """Initialize our craton authentication class.""" - self.username = username - self.project_id = project_id - self.token = token - - def get_token(self, session, **kwargs): - """Return our token.""" - return self.token - - def get_headers(self, session, **kwargs): - """Return the craton authentication headers.""" - headers = super(CratonAuth, self).get_headers(session, **kwargs) - if headers is None: - # NOTE(sigmavirus24): This means that the token must be None. We - # should not allow this to go further. We're using built-in Craton - # authentication (not authenticating against Keystone) so we will - # be unable to authenticate. - raise exc.UnableToAuthenticate() - - headers['X-Auth-User'] = self.username - headers['X-Auth-Project'] = '{}'.format(self.project_id) - return headers diff --git a/cratonclient/tests/integration/test_auth.py b/cratonclient/tests/integration/test_auth.py new file mode 100644 index 0000000..bcba975 --- /dev/null +++ b/cratonclient/tests/integration/test_auth.py @@ -0,0 +1,59 @@ +# Copyright (c) 2016 Rackspace +# +# 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. +"""Integration tests for the cratonclient.auth module.""" +from oslo_utils import uuidutils + +from keystoneauth1.identity.v3 import password as ksa_password +from keystoneauth1 import session as ksa_session + +from cratonclient import auth +from cratonclient import session +from cratonclient.tests import base + +PROJECT_ID = uuidutils.generate_uuid() + + +class TestAuth(base.TestCase): + """Integration tests for the auth module functions.""" + + def test_craton_auth_configures_craton_session(self): + """Verify the configuration of a cratonclient Session.""" + new_session = auth.craton_auth( + username='demo', + token='demo', + project_id=PROJECT_ID, + ) + + self.assertIsInstance(new_session, session.Session) + + keystone_session = new_session._session + self.assertIsInstance(keystone_session, ksa_session.Session) + self.assertIsInstance(keystone_session.auth, auth.CratonAuth) + + def test_keystone_auth_configures_craton_session(self): + """Verify the configuration of a cratonclient Session.""" + new_session = auth.keystone_auth( + auth_url='https://identity.openstack.org/v3', + username='admin', + password='adminPassword', + project_id=PROJECT_ID, + project_domain_name='Default', + user_domain_name='Default', + ) + + self.assertIsInstance(new_session, session.Session) + + keystone_session = new_session._session + self.assertIsInstance(keystone_session, ksa_session.Session) + self.assertIsInstance(keystone_session.auth, ksa_password.Password) diff --git a/cratonclient/tests/unit/test_auth.py b/cratonclient/tests/unit/test_auth.py new file mode 100644 index 0000000..b13558e --- /dev/null +++ b/cratonclient/tests/unit/test_auth.py @@ -0,0 +1,197 @@ +# Copyright (c) 2016 Rackspace +# +# 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. +"""Unit tests for the cratonclient.auth module.""" +import mock + +from oslo_utils import uuidutils + +from cratonclient import auth +from cratonclient.tests import base + +USERNAME = 'test' +TOKEN = 'fake-token' +PROJECT_ID = uuidutils.generate_uuid() + + +class TestCreateSessionWith(base.TestCase): + """"Tests for the create_session_with function.""" + + def setUp(self): + """Set up mocks to test the create_session_with function.""" + super(TestCreateSessionWith, self).setUp() + self._session_mock = mock.patch('cratonclient.session.Session') + self.session_class = self._session_mock.start() + self.addCleanup(self._session_mock.stop) + + self._ksa_session_mock = mock.patch('keystoneauth1.session.Session') + self.ksa_session_class = self._ksa_session_mock.start() + self.addCleanup(self._ksa_session_mock.stop) + + def test_creates_sessions(self): + """Verify we create cratonclient and keystoneauth Sesssions.""" + auth_plugin = mock.Mock() + auth.create_session_with(auth_plugin, True) + + self.ksa_session_class.assert_called_once_with( + auth=auth_plugin, + verify=True, + ) + self.session_class.assert_called_once_with( + session=self.ksa_session_class.return_value + ) + + +class TestCratonAuth(base.TestCase): + """Tests for the craton_auth function.""" + + def setUp(self): + """Set up mocks to test the craton_auth function.""" + super(TestCratonAuth, self).setUp() + self._create_session_with_mock = mock.patch( + 'cratonclient.auth.create_session_with' + ) + self.create_session_with = self._create_session_with_mock.start() + self.addCleanup(self._create_session_with_mock.stop) + + self._craton_auth_mock = mock.patch('cratonclient.auth.CratonAuth') + self.craton_auth_class = self._craton_auth_mock.start() + self.addCleanup(self._craton_auth_mock.stop) + + def test_creates_craton_auth_ksa_plugin(self): + """Verify we create a new instance of CratonAuth.""" + auth.craton_auth( + username='demo', + token='demo', + project_id=PROJECT_ID, + ) + + self.craton_auth_class.assert_called_once_with( + username='demo', + token='demo', + project_id=PROJECT_ID, + ) + + def test_calls_create_session_with(self): + """Verify we call create_session_with using the right parameters.""" + auth.craton_auth( + username='demo', + token='demo', + project_id=PROJECT_ID, + verify=False, + ) + + self.create_session_with.assert_called_once_with( + self.craton_auth_class.return_value, False + ) + + +class TestKeystoneAuth(base.TestCase): + """Tests for the keystone_auth function.""" + + def setUp(self): + """Set up mocks to test the keystone_auth function.""" + super(TestKeystoneAuth, self).setUp() + self._create_session_with_mock = mock.patch( + 'cratonclient.auth.create_session_with' + ) + self.create_session_with = self._create_session_with_mock.start() + self.addCleanup(self._create_session_with_mock.stop) + + self._ksa_password_mock = mock.patch( + 'keystoneauth1.identity.v3.password.Password' + ) + self.ksa_password_class = self._ksa_password_mock.start() + self.addCleanup(self._ksa_password_mock.stop) + + def test_creates_ksa_password_plugin(self): + """Verify we create a Password keystoneauth plugin.""" + auth.keystone_auth( + auth_url='https://identity.openstack.org/v3', + username='admin', + password='adminPassword', + project_name='admin', + project_domain_name='Default', + user_domain_name='Default', + ) + + self.ksa_password_class.assert_called_once_with( + auth_url='https://identity.openstack.org/v3', + username='admin', + password='adminPassword', + project_name='admin', + project_domain_name='Default', + user_domain_name='Default', + project_id=None, + project_domain_id=None, + user_domain_id=None, + ) + + def test_calls_create_session_with(self): + """Verify we call create_session_with using the right parameters.""" + auth.keystone_auth( + auth_url='https://identity.openstack.org/v3', + username='admin', + password='adminPassword', + project_name='admin', + project_domain_name='Default', + user_domain_name='Default', + verify=False, + ) + + self.create_session_with.assert_called_once_with( + self.ksa_password_class.return_value, False + ) + + +class TestCratonAuthPlugin(base.TestCase): + """Craton authentication keystoneauth plugin tests.""" + + def test_stores_authentication_details(self): + """Verify that our plugin stores auth details.""" + plugin = auth.CratonAuth( + username=USERNAME, + project_id=PROJECT_ID, + token=TOKEN, + ) + self.assertEqual(USERNAME, plugin.username) + self.assertEqual(PROJECT_ID, plugin.project_id) + self.assertEqual(TOKEN, plugin.token) + + def test_generates_appropriate_headers(self): + """Verify we generate the X-Auth-* headers.""" + fake_session = object() + plugin = auth.CratonAuth( + username=USERNAME, + project_id=PROJECT_ID, + token=TOKEN, + ) + self.assertDictEqual( + { + 'X-Auth-Token': TOKEN, + 'X-Auth-User': USERNAME, + 'X-Auth-Project': '{}'.format(PROJECT_ID), + }, + plugin.get_headers(fake_session) + ) + + def test_stores_token(self): + """Verify get_token returns our token.""" + fake_session = object() + plugin = auth.CratonAuth( + username=USERNAME, + project_id=PROJECT_ID, + token=TOKEN, + ) + + self.assertEqual(TOKEN, plugin.get_token(fake_session)) diff --git a/cratonclient/tests/unit/test_session.py b/cratonclient/tests/unit/test_session.py index ee79497..6055d7c 100644 --- a/cratonclient/tests/unit/test_session.py +++ b/cratonclient/tests/unit/test_session.py @@ -14,6 +14,7 @@ """Session specific unit tests.""" from keystoneauth1 import session as ksa_session +from cratonclient import auth from cratonclient import session from cratonclient.tests import base @@ -28,9 +29,9 @@ class TestCratonAuth(base.TestCase): def test_stores_authentication_details(self): """Verify that our plugin stores auth details.""" - plugin = session.CratonAuth(username=TEST_USERNAME_0, - project_id=TEST_PROJECT_0, - token=TEST_TOKEN_0) + plugin = auth.CratonAuth(username=TEST_USERNAME_0, + project_id=TEST_PROJECT_0, + token=TEST_TOKEN_0) self.assertEqual(TEST_USERNAME_0, plugin.username) self.assertEqual(TEST_PROJECT_0, plugin.project_id) self.assertEqual(TEST_TOKEN_0, plugin.token) @@ -38,9 +39,9 @@ class TestCratonAuth(base.TestCase): def test_generates_appropriate_headers(self): """Verify we generate the X-Auth-* headers.""" fake_session = object() - plugin = session.CratonAuth(username=TEST_USERNAME_0, - project_id=TEST_PROJECT_0, - token=TEST_TOKEN_0) + plugin = auth.CratonAuth(username=TEST_USERNAME_0, + project_id=TEST_PROJECT_0, + token=TEST_TOKEN_0) self.assertDictEqual( { 'X-Auth-Token': TEST_TOKEN_0, @@ -53,9 +54,9 @@ class TestCratonAuth(base.TestCase): def test_stores_token(self): """Verify get_token returns our token.""" fake_session = object() - plugin = session.CratonAuth(username=TEST_USERNAME_0, - project_id=TEST_PROJECT_0, - token=TEST_TOKEN_0) + plugin = auth.CratonAuth(username=TEST_USERNAME_0, + project_id=TEST_PROJECT_0, + token=TEST_TOKEN_0) self.assertEqual(TEST_TOKEN_0, plugin.get_token(fake_session)) @@ -63,14 +64,6 @@ class TestCratonAuth(base.TestCase): class TestSession(base.TestCase): """Unit tests for cratonclient's Session abstraction.""" - def test_creates_craton_auth_plugin(self): - """Verify we default to using keystoneauth plugin auth.""" - craton_session = session.Session(username=TEST_USERNAME_0, - project_id=TEST_PROJECT_0, - token=TEST_TOKEN_0) - - self.assertIsInstance(craton_session._auth, session.CratonAuth) - def test_creates_keystoneauth_session(self): """Verify we default to keystoneauth sessions and semantics.""" craton_session = session.Session(username=TEST_USERNAME_0, diff --git a/doc/source/authentication-documentation.rst b/doc/source/authentication-documentation.rst new file mode 100644 index 0000000..23a6759 --- /dev/null +++ b/doc/source/authentication-documentation.rst @@ -0,0 +1,7 @@ +================================= + cratonclient.auth Documentation +================================= + +.. autofunction:: cratonclient.auth.craton_auth + +.. autofunction:: cratonclient.auth.keystone_auth diff --git a/doc/source/index.rst b/doc/source/index.rst index 0b6c218..509bde8 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,8 +3,9 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to python-cratonclient's documentation! -======================================================== +================================================= + Welcome to python-cratonclient's documentation! +================================================= Contents: @@ -15,6 +16,7 @@ Contents: installation usage v1-api-documentation + authentication-documentation contributing Indices and tables diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 062b6c5..2708cfa 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -35,12 +35,12 @@ authentication, you need only do the following: .. code-block:: python - from cratonclient import session + from cratonclient import auth from cratonclient.v1 import client - craton_session = session.Session( + craton_session = auth.craton_auth( username=USERNAME, - password=TOKEN, + token=TOKEN, project_id=PROJECT_ID, ) @@ -74,10 +74,10 @@ Then, we need to do the following: from keystoneauth1.identity.v3 import password as password_auth from keystoneauth1 import session as ksa_session - from cratonclient import session + from cratonclient import auth from cratonclient.v1 import client - _auth = password_auth.Password( + craton_session = auth.keystone_auth( auth_url=AUTH_URL, password=PASSWORD, username=USERNAME, @@ -85,7 +85,6 @@ Then, we need to do the following: project_name=PROJECT_NAME, project_domain_name=PROJECT_DOMAIN_NAME, ) - craton_session = session.Session(session=ksa_session.Session(auth=_auth)) craton = client.Client( session=craton_session, url=URL,