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
This commit is contained in:
Terry Howe 2014-04-17 20:28:58 -06:00
parent f47198fca6
commit 40b23d8299
7 changed files with 285 additions and 0 deletions

View File

55
openstack/auth/base.py Normal file
View File

@ -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.
"""

32
openstack/auth/service.py Normal file
View File

@ -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

75
openstack/session.py Normal file
View File

@ -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)

25
openstack/tests/fakes.py Normal file
View File

@ -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

View File

@ -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)

View File

@ -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