From 40b23d8299d6f880bace09bde2ae5b00519a6ab4 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 17 Apr 2014 20:28:58 -0600 Subject: [PATCH] Session layer with base authenticator The session layer provides an authenticated HTTP layer for the SDK. The session is created with a transport and an authenticator. All requests are passed a service identifier as well as the normal HTTP parameters. The authenticator is used to get the token and get the endpoint for the service. The service identifier is used to get the endpoint associated with a service. The service identifier has a service type, visibiltiy and region. This code is based on the keystoneclient, but it has been simplified and some renaming has been done. Change-Id: I7184c733a5593dffb07ce4fc77e126a043274fea --- openstack/auth/__init__.py | 0 openstack/auth/base.py | 55 +++++++++++++++++++ openstack/auth/service.py | 32 +++++++++++ openstack/session.py | 75 +++++++++++++++++++++++++ openstack/tests/fakes.py | 25 +++++++++ openstack/tests/test_session.py | 97 +++++++++++++++++++++++++++++++++ test-requirements.txt | 1 + 7 files changed, 285 insertions(+) create mode 100644 openstack/auth/__init__.py create mode 100644 openstack/auth/base.py create mode 100644 openstack/auth/service.py create mode 100644 openstack/session.py create mode 100644 openstack/tests/fakes.py create mode 100644 openstack/tests/test_session.py diff --git a/openstack/auth/__init__.py b/openstack/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/auth/base.py b/openstack/auth/base.py new file mode 100644 index 000000000..2534d52bf --- /dev/null +++ b/openstack/auth/base.py @@ -0,0 +1,55 @@ +# 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 abc +import six + + +@six.add_metaclass(abc.ABCMeta) +class BaseAuthenticator(object): + """The basic structure of an authenticator.""" + + @abc.abstractmethod + def get_token(self, transport, **kwargs): + """Obtain a token. + + How the token is obtained is up to the authenticator. If it is still + valid it may be re-used, retrieved from cache or invoke an + authentication request against a server. + + There are no required kwargs. They are implementation specific to + an authenticator. + + An authenticator may raise an exception if it fails to retrieve a + token. + + :param transport: A transport object so the authenticator can make + HTTP calls. + :return string: A token to use. + """ + + @abc.abstractmethod + def get_endpoint(self, transport, service, **kwargs): + """Return an endpoint for the client. + + There are no required keyword arguments to ``get_endpoint`` as an + authenticator should use best effort with the information available to + determine the endpoint. + + :param Transport transport: A transport object so the authenticator + can make HTTP calls + :param ServiceIdentifier service: The object that identifies the + service for the authenticator. + + :returns string: The base URL that will be used to talk to the + required service or None if not available. + """ diff --git a/openstack/auth/service.py b/openstack/auth/service.py new file mode 100644 index 000000000..1e8793182 --- /dev/null +++ b/openstack/auth/service.py @@ -0,0 +1,32 @@ +# 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. + + +class ServiceIdentifier(object): + """The basic structure of an authentication plugin.""" + + PUBLIC = 'public' + INTERNAL = 'internal' + ADMIN = 'admin' + VISIBILITY = [PUBLIC, INTERNAL, ADMIN] + + def __init__(self, service_type, visibility=PUBLIC, region=None): + """" Create a service identifier. + + :param string service_type: The desired type of service. + :param string visibility: The exposure of the endpoint. Should be + `public` (default), `internal` or `admin`. + :param string region: The desired region (optional). + """ + self.service_type = service_type + self.visibility = visibility + self.region = region diff --git a/openstack/session.py b/openstack/session.py new file mode 100644 index 000000000..fcecf70ba --- /dev/null +++ b/openstack/session.py @@ -0,0 +1,75 @@ +# 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 logging + + +_logger = logging.getLogger(__name__) + + +class Session(object): + + def __init__(self, transport, authenticator): + """Maintains client communication session. + + Session layer which uses the transport for communication. The + authenticator also uses the transport to keep authenticated. + + :param transport: A transport layer for the session. + :param authenticator: An authenticator to authenticate the session. + """ + self.transport = transport + self.authenticator = authenticator + + def _request(self, service, path, method, authenticate=True, **kwargs): + """Send an HTTP request with the specified characteristics. + + Handle a session level request. + + :param ServiceIdentifier service: Object that identifies service to + the authenticator. + :type service: :class:`openstack.auth.service.ServiceIdentifier` + :param string path: Path relative to authentictor base url. + :param string method: The http method to use. (eg. 'GET', 'POST'). + :param bool authenticate: True if a token should be attached + :param kwargs: any other parameter that can be passed to transport + and authenticator. + + :returns: The response to the request. + """ + + headers = kwargs.setdefault('headers', dict()) + if authenticate: + token = self.authenticator.get_token(self.transport) + if token: + headers['X-Auth-Token'] = token + + url = self.authenticator.get_endpoint(self.transport, service) + path + return self.transport.request(method, url, **kwargs) + + def head(self, service, path, **kwargs): + return self._request(service, path, 'HEAD', **kwargs) + + def get(self, service, path, **kwargs): + return self._request(service, path, 'GET', **kwargs) + + def post(self, service, path, **kwargs): + return self._request(service, path, 'POST', **kwargs) + + def put(self, service, path, **kwargs): + return self._request(service, path, 'PUT', **kwargs) + + def delete(self, service, path, **kwargs): + return self._request(service, path, 'DELETE', **kwargs) + + def patch(self, service, path, **kwargs): + return self._request(service, path, 'PATCH', **kwargs) diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py new file mode 100644 index 000000000..116bd8471 --- /dev/null +++ b/openstack/tests/fakes.py @@ -0,0 +1,25 @@ +# 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. + +import mock + + +class FakeTransport(mock.Mock): + RESPONSE = mock.Mock('200 OK') + + def __init__(self): + super(FakeTransport, self).__init__() + self.request = mock.Mock() + self.request.return_value = self.RESPONSE diff --git a/openstack/tests/test_session.py b/openstack/tests/test_session.py new file mode 100644 index 000000000..930a274c3 --- /dev/null +++ b/openstack/tests/test_session.py @@ -0,0 +1,97 @@ +# 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 mock + +from openstack.auth import service +from openstack import session +from openstack.tests import base +from openstack.tests import fakes + + +class FakeAuthenticator(mock.Mock): + TOKEN = 'fake_token' + ENDPOINT = 'http://www.example.com/endpoint' + + def __init__(self): + super(FakeAuthenticator, self).__init__() + self.get_token = mock.Mock() + self.get_token.return_value = self.TOKEN + self.get_endpoint = mock.Mock() + self.get_endpoint.return_value = self.ENDPOINT + + +class TestSession(base.TestCase): + + TEST_PATH = '/test/path' + + def setUp(self): + super(TestSession, self).setUp() + self.xport = fakes.FakeTransport() + self.auth = FakeAuthenticator() + self.serv = service.ServiceIdentifier('identity') + self.sess = session.Session(self.xport, self.auth) + self.expected = {'headers': {'X-Auth-Token': self.auth.TOKEN}} + + def test_head(self): + resp = self.sess.head(self.serv, self.TEST_PATH) + + self.assertEqual(self.xport.RESPONSE, resp) + self.auth.get_token.assert_called_with(self.xport) + self.auth.get_endpoint.assert_called_with(self.xport, self.serv) + url = self.auth.ENDPOINT + self.TEST_PATH + self.xport.request.assert_called_with('HEAD', url, **self.expected) + + def test_get(self): + resp = self.sess.get(self.serv, self.TEST_PATH) + + self.assertEqual(self.xport.RESPONSE, resp) + self.auth.get_token.assert_called_with(self.xport) + self.auth.get_endpoint.assert_called_with(self.xport, self.serv) + url = self.auth.ENDPOINT + self.TEST_PATH + self.xport.request.assert_called_with('GET', url, **self.expected) + + def test_post(self): + resp = self.sess.post(self.serv, self.TEST_PATH) + + self.assertEqual(self.xport.RESPONSE, resp) + self.auth.get_token.assert_called_with(self.xport) + self.auth.get_endpoint.assert_called_with(self.xport, self.serv) + url = self.auth.ENDPOINT + self.TEST_PATH + self.xport.request.assert_called_with('POST', url, **self.expected) + + def test_put(self): + resp = self.sess.put(self.serv, self.TEST_PATH) + + self.assertEqual(self.xport.RESPONSE, resp) + self.auth.get_token.assert_called_with(self.xport) + self.auth.get_endpoint.assert_called_with(self.xport, self.serv) + url = self.auth.ENDPOINT + self.TEST_PATH + self.xport.request.assert_called_with('PUT', url, **self.expected) + + def test_delete(self): + resp = self.sess.delete(self.serv, self.TEST_PATH) + + self.assertEqual(self.xport.RESPONSE, resp) + self.auth.get_token.assert_called_with(self.xport) + self.auth.get_endpoint.assert_called_with(self.xport, self.serv) + url = self.auth.ENDPOINT + self.TEST_PATH + self.xport.request.assert_called_with('DELETE', url, **self.expected) + + def test_patch(self): + resp = self.sess.patch(self.serv, self.TEST_PATH) + + self.assertEqual(self.xport.RESPONSE, resp) + self.auth.get_token.assert_called_with(self.xport) + self.auth.get_endpoint.assert_called_with(self.xport, self.serv) + url = self.auth.ENDPOINT + self.TEST_PATH + self.xport.request.assert_called_with('PATCH', url, **self.expected) diff --git a/test-requirements.txt b/test-requirements.txt index 71d6af893..98de8ee8d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ hacking>=0.0.8,<0.9 coverage>=3.6 discover fixtures>=0.3.14 +mock>=1.0 httpretty>=0.8.0 python-subunit sphinx>=1.1.2