Create Authentication Plugins
Provides the framework for creating authentication plugins and using them from a session object. To allow this system to co-exist with the original client there is a bit of a hack. The client object itself is now also an authentication plugin, that supports the original client pattern. If a client is created without a session object then that session object uses the client as it's authentication plugin. Change-Id: I682c8dcd3705148aaa804a91f4ed48a5b74bdc12 blueprint: auth-plugins
This commit is contained in:
parent
1263bd7c3a
commit
96267731ec
|
@ -0,0 +1,38 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 BaseAuthPlugin(object):
|
||||
"""The basic structure of an authentication plugin."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_token(self, session, **kwargs):
|
||||
"""Obtain a token.
|
||||
|
||||
How the token is obtained is up to the plugin. 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 passed directly to the auth
|
||||
plugin and they are implementation specific.
|
||||
|
||||
Returning None will indicate that no token was able to be retrieved.
|
||||
|
||||
:param session: A session object so the plugin can make HTTP calls.
|
||||
:return string: A token to use.
|
||||
"""
|
|
@ -0,0 +1,60 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 logging
|
||||
import six
|
||||
|
||||
from keystoneclient.auth import base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseIdentityPlugin(base.BaseAuthPlugin):
|
||||
|
||||
def __init__(self,
|
||||
auth_url=None,
|
||||
username=None,
|
||||
password=None,
|
||||
token=None,
|
||||
trust_id=None):
|
||||
|
||||
super(BaseIdentityPlugin, self).__init__()
|
||||
|
||||
self.auth_url = auth_url
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.token = token
|
||||
self.trust_id = trust_id
|
||||
|
||||
self.auth_ref = None
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_auth_ref(self, session, **kwargs):
|
||||
"""Obtain a token from an OpenStack Identity Service.
|
||||
|
||||
This method is overridden by the various token version plugins.
|
||||
|
||||
This function should not be called independently and is expected to be
|
||||
invoked via the do_authenticate function.
|
||||
|
||||
:returns AccessInfo: Token access information.
|
||||
"""
|
||||
|
||||
def get_token(self, session, **kwargs):
|
||||
if not self.auth_ref or self.auth_ref.will_expire_soon(1):
|
||||
self.auth_ref = self.get_auth_ref(session, **kwargs)
|
||||
|
||||
return self.auth_ref.auth_token
|
|
@ -0,0 +1,41 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 Client(object):
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
kwargs.setdefault('authenticated', True)
|
||||
return self.session.request(url, method, **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self.request(url, 'GET', **kwargs)
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.request(url, 'HEAD', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.request(url, 'PUT', **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.request(url, 'PATCH', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.request(url, 'DELETE', **kwargs)
|
|
@ -49,3 +49,7 @@ class DiscoveryFailure(ClientException):
|
|||
|
||||
class VersionNotAvailable(DiscoveryFailure):
|
||||
"""Discovery failed as the version you requested is not available."""
|
||||
|
||||
|
||||
class MissingAuthPlugin(ClientException):
|
||||
"""An authenticated request is required but no plugin available."""
|
||||
|
|
|
@ -39,6 +39,8 @@ if not hasattr(urlparse, 'parse_qsl'):
|
|||
|
||||
|
||||
from keystoneclient import access
|
||||
from keystoneclient.auth import base
|
||||
from keystoneclient import baseclient
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient.openstack.common import jsonutils
|
||||
from keystoneclient import session as client_session
|
||||
|
@ -52,7 +54,7 @@ USER_AGENT = client_session.USER_AGENT
|
|||
request = client_session.request
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
class HTTPClient(baseclient.Client, base.BaseAuthPlugin):
|
||||
|
||||
def __init__(self, username=None, tenant_id=None, tenant_name=None,
|
||||
password=None, auth_url=None, region_name=None, endpoint=None,
|
||||
|
@ -121,7 +123,6 @@ class HTTPClient(object):
|
|||
|
||||
"""
|
||||
# set baseline defaults
|
||||
|
||||
self.user_id = None
|
||||
self.username = None
|
||||
self.user_domain_id = None
|
||||
|
@ -223,8 +224,9 @@ class HTTPClient(object):
|
|||
|
||||
if not session:
|
||||
session = client_session.Session.construct(kwargs)
|
||||
session.auth = self
|
||||
|
||||
self.session = session
|
||||
super(HTTPClient, self).__init__(session=session)
|
||||
self.domain = ''
|
||||
self.debug_log = debug
|
||||
|
||||
|
@ -236,6 +238,9 @@ class HTTPClient(object):
|
|||
self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION
|
||||
self.stale_duration = int(self.stale_duration)
|
||||
|
||||
def get_token(self, session, **kwargs):
|
||||
return self.auth_token
|
||||
|
||||
@property
|
||||
def auth_token(self):
|
||||
if self._auth_token:
|
||||
|
@ -377,14 +382,21 @@ class HTTPClient(object):
|
|||
if auth_ref is None or self.force_new_token:
|
||||
new_token_needed = True
|
||||
kwargs['password'] = password
|
||||
resp, body = self.get_raw_token_from_identity_service(**kwargs)
|
||||
resp = self.get_raw_token_from_identity_service(**kwargs)
|
||||
|
||||
# TODO(jamielennox): passing region_name here is wrong but required
|
||||
# for backwards compatibility. Deprecate and provide warning.
|
||||
self.auth_ref = access.AccessInfo.factory(resp, body,
|
||||
region_name=region_name)
|
||||
if isinstance(resp, access.AccessInfo):
|
||||
self.auth_ref = resp
|
||||
else:
|
||||
self.auth_ref = access.AccessInfo.factory(*resp)
|
||||
|
||||
# NOTE(jamielennox): The original client relies on being able to
|
||||
# push the region name into the service catalog but new auth
|
||||
# it in.
|
||||
if region_name:
|
||||
self.auth_ref.service_catalog._region_name = region_name
|
||||
else:
|
||||
self.auth_ref = auth_ref
|
||||
|
||||
self.process_token(region_name=region_name)
|
||||
if new_token_needed:
|
||||
self.store_auth_ref_into_keyring(keyring_key)
|
||||
|
@ -540,7 +552,8 @@ class HTTPClient(object):
|
|||
except KeyError:
|
||||
pass
|
||||
|
||||
resp = self.session.request(url, method, **kwargs)
|
||||
kwargs.setdefault('authenticated', False)
|
||||
resp = super(HTTPClient, self).request(url, method, **kwargs)
|
||||
return resp, self._decode_body(resp)
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
|
@ -559,13 +572,9 @@ class HTTPClient(object):
|
|||
if is_management:
|
||||
url_to_use = self.management_url
|
||||
|
||||
kwargs.setdefault('headers', {})
|
||||
if self.auth_token:
|
||||
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
||||
|
||||
resp, body = self.request(url_to_use + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
kwargs.setdefault('authenticated', None)
|
||||
return self.request(url_to_use + url, method,
|
||||
**kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
|
|
@ -37,14 +37,18 @@ class Session(object):
|
|||
REDIRECT_STATUSES = (301, 302, 303, 305, 307)
|
||||
DEFAULT_REDIRECT_LIMIT = 30
|
||||
|
||||
def __init__(self, session=None, original_ip=None, verify=True, cert=None,
|
||||
timeout=None, user_agent=None,
|
||||
def __init__(self, auth=None, session=None, original_ip=None, verify=True,
|
||||
cert=None, timeout=None, user_agent=None,
|
||||
redirect=DEFAULT_REDIRECT_LIMIT):
|
||||
"""Maintains client communication state and common functionality.
|
||||
|
||||
As much as possible the parameters to this class reflect and are passed
|
||||
directly to the requests library.
|
||||
|
||||
:param auth: An authentication plugin to authenticate the session with.
|
||||
(optional, defaults to None)
|
||||
:param requests.Session session: A requests session object that can be
|
||||
used for issuing requests. (optional)
|
||||
:param string original_ip: The original IP of the requesting user
|
||||
which will be sent to identity service in a
|
||||
'Forwarded' header. (optional)
|
||||
|
@ -74,6 +78,7 @@ class Session(object):
|
|||
if not session:
|
||||
session = requests.Session()
|
||||
|
||||
self.auth = auth
|
||||
self.session = session
|
||||
self.original_ip = original_ip
|
||||
self.verify = verify
|
||||
|
@ -89,7 +94,8 @@ class Session(object):
|
|||
self.user_agent = user_agent
|
||||
|
||||
def request(self, url, method, json=None, original_ip=None,
|
||||
user_agent=None, redirect=None, **kwargs):
|
||||
user_agent=None, redirect=None, authenticated=None,
|
||||
**kwargs):
|
||||
"""Send an HTTP request with the specified characteristics.
|
||||
|
||||
Wrapper around `requests.Session.request` to handle tasks such as
|
||||
|
@ -111,6 +117,10 @@ class Session(object):
|
|||
can be followed by a request. Either an
|
||||
integer for a specific count or True/False
|
||||
for forever/never. (optional)
|
||||
:param bool authenticated: True if a token should be attached to this
|
||||
request, False if not or None for attach if
|
||||
an auth_plugin is available.
|
||||
(optional, defaults to None)
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
requests.Session.request (such as `headers`). Except:
|
||||
'data' will be overwritten by the data in 'json' param.
|
||||
|
@ -125,6 +135,17 @@ class Session(object):
|
|||
|
||||
headers = kwargs.setdefault('headers', dict())
|
||||
|
||||
if authenticated is None:
|
||||
authenticated = self.auth is not None
|
||||
|
||||
if authenticated:
|
||||
token = self.get_token()
|
||||
|
||||
if not token:
|
||||
raise exceptions.AuthorizationFailure("No token Available")
|
||||
|
||||
headers['X-Auth-Token'] = token
|
||||
|
||||
if self.cert:
|
||||
kwargs.setdefault('cert', self.cert)
|
||||
|
||||
|
@ -286,3 +307,10 @@ class Session(object):
|
|||
session=kwargs.pop('session', None),
|
||||
original_ip=kwargs.pop('original_ip', None),
|
||||
user_agent=kwargs.pop('user_agent', None))
|
||||
|
||||
def get_token(self):
|
||||
"""Return a token as provided by the auth plugin."""
|
||||
if not self.auth:
|
||||
raise exceptions.MissingAuthPlugin("Token Required")
|
||||
|
||||
return self.auth.get_token(self)
|
||||
|
|
|
@ -17,6 +17,7 @@ import httpretty
|
|||
import mock
|
||||
import requests
|
||||
|
||||
from keystoneclient.auth import base
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient import session as client_session
|
||||
from keystoneclient.tests import utils
|
||||
|
@ -252,3 +253,47 @@ class ConstructSessionFromArgsTests(utils.TestCase):
|
|||
args = {key: value}
|
||||
self.assertEqual(getattr(self._s(args), key), value)
|
||||
self.assertNotIn(key, args)
|
||||
|
||||
|
||||
class AuthPlugin(base.BaseAuthPlugin):
|
||||
"""Very simple debug authentication plugin.
|
||||
|
||||
Takes Parameters such that it can throw exceptions at the right times.
|
||||
"""
|
||||
|
||||
TEST_TOKEN = 'aToken'
|
||||
|
||||
def __init__(self, token=TEST_TOKEN):
|
||||
self.token = token
|
||||
|
||||
def get_token(self, session):
|
||||
return self.token
|
||||
|
||||
|
||||
class SessionAuthTests(utils.TestCase):
|
||||
|
||||
TEST_URL = 'http://127.0.0.1:5000/'
|
||||
TEST_JSON = {'hello': 'world'}
|
||||
|
||||
@httpretty.activate
|
||||
def test_auth_plugin_default_with_plugin(self):
|
||||
self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON)
|
||||
|
||||
# if there is an auth_plugin then it should default to authenticated
|
||||
auth = AuthPlugin()
|
||||
sess = client_session.Session(auth=auth)
|
||||
resp = sess.get(self.TEST_URL)
|
||||
self.assertDictEqual(resp.json(), self.TEST_JSON)
|
||||
|
||||
self.assertRequestHeaderEqual('X-Auth-Token', AuthPlugin.TEST_TOKEN)
|
||||
|
||||
@httpretty.activate
|
||||
def test_auth_plugin_disable(self):
|
||||
self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON)
|
||||
|
||||
auth = AuthPlugin()
|
||||
sess = client_session.Session(auth=auth)
|
||||
resp = sess.get(self.TEST_URL, authenticated=False)
|
||||
self.assertDictEqual(resp.json(), self.TEST_JSON)
|
||||
|
||||
self.assertRequestHeaderEqual('X-Auth-Token', None)
|
||||
|
|
Loading…
Reference in New Issue