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