Merge "Create Authentication Plugins"
This commit is contained in:
0
keystoneclient/auth/__init__.py
Normal file
0
keystoneclient/auth/__init__.py
Normal file
38
keystoneclient/auth/base.py
Normal file
38
keystoneclient/auth/base.py
Normal file
@@ -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
keystoneclient/auth/identity/__init__.py
Normal file
0
keystoneclient/auth/identity/__init__.py
Normal file
60
keystoneclient/auth/identity/base.py
Normal file
60
keystoneclient/auth/identity/base.py
Normal file
@@ -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
|
41
keystoneclient/baseclient.py
Normal file
41
keystoneclient/baseclient.py
Normal file
@@ -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):
|
class VersionNotAvailable(DiscoveryFailure):
|
||||||
"""Discovery failed as the version you requested is not available."""
|
"""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 import access
|
||||||
|
from keystoneclient.auth import base
|
||||||
|
from keystoneclient import baseclient
|
||||||
from keystoneclient import exceptions
|
from keystoneclient import exceptions
|
||||||
from keystoneclient.openstack.common import jsonutils
|
from keystoneclient.openstack.common import jsonutils
|
||||||
from keystoneclient import session as client_session
|
from keystoneclient import session as client_session
|
||||||
@@ -52,7 +54,7 @@ USER_AGENT = client_session.USER_AGENT
|
|||||||
request = client_session.request
|
request = client_session.request
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(object):
|
class HTTPClient(baseclient.Client, base.BaseAuthPlugin):
|
||||||
|
|
||||||
def __init__(self, username=None, tenant_id=None, tenant_name=None,
|
def __init__(self, username=None, tenant_id=None, tenant_name=None,
|
||||||
password=None, auth_url=None, region_name=None, endpoint=None,
|
password=None, auth_url=None, region_name=None, endpoint=None,
|
||||||
@@ -121,7 +123,6 @@ class HTTPClient(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# set baseline defaults
|
# set baseline defaults
|
||||||
|
|
||||||
self.user_id = None
|
self.user_id = None
|
||||||
self.username = None
|
self.username = None
|
||||||
self.user_domain_id = None
|
self.user_domain_id = None
|
||||||
@@ -223,8 +224,9 @@ class HTTPClient(object):
|
|||||||
|
|
||||||
if not session:
|
if not session:
|
||||||
session = client_session.Session.construct(kwargs)
|
session = client_session.Session.construct(kwargs)
|
||||||
|
session.auth = self
|
||||||
|
|
||||||
self.session = session
|
super(HTTPClient, self).__init__(session=session)
|
||||||
self.domain = ''
|
self.domain = ''
|
||||||
self.debug_log = debug
|
self.debug_log = debug
|
||||||
|
|
||||||
@@ -236,6 +238,9 @@ class HTTPClient(object):
|
|||||||
self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION
|
self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION
|
||||||
self.stale_duration = int(self.stale_duration)
|
self.stale_duration = int(self.stale_duration)
|
||||||
|
|
||||||
|
def get_token(self, session, **kwargs):
|
||||||
|
return self.auth_token
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auth_token(self):
|
def auth_token(self):
|
||||||
if self._auth_token:
|
if self._auth_token:
|
||||||
@@ -377,14 +382,21 @@ class HTTPClient(object):
|
|||||||
if auth_ref is None or self.force_new_token:
|
if auth_ref is None or self.force_new_token:
|
||||||
new_token_needed = True
|
new_token_needed = True
|
||||||
kwargs['password'] = password
|
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
|
if isinstance(resp, access.AccessInfo):
|
||||||
# for backwards compatibility. Deprecate and provide warning.
|
self.auth_ref = resp
|
||||||
self.auth_ref = access.AccessInfo.factory(resp, body,
|
else:
|
||||||
region_name=region_name)
|
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:
|
else:
|
||||||
self.auth_ref = auth_ref
|
self.auth_ref = auth_ref
|
||||||
|
|
||||||
self.process_token(region_name=region_name)
|
self.process_token(region_name=region_name)
|
||||||
if new_token_needed:
|
if new_token_needed:
|
||||||
self.store_auth_ref_into_keyring(keyring_key)
|
self.store_auth_ref_into_keyring(keyring_key)
|
||||||
@@ -540,7 +552,8 @@ class HTTPClient(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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)
|
return resp, self._decode_body(resp)
|
||||||
|
|
||||||
def _cs_request(self, url, method, **kwargs):
|
def _cs_request(self, url, method, **kwargs):
|
||||||
@@ -559,13 +572,9 @@ class HTTPClient(object):
|
|||||||
if is_management:
|
if is_management:
|
||||||
url_to_use = self.management_url
|
url_to_use = self.management_url
|
||||||
|
|
||||||
kwargs.setdefault('headers', {})
|
kwargs.setdefault('authenticated', None)
|
||||||
if self.auth_token:
|
return self.request(url_to_use + url, method,
|
||||||
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
**kwargs)
|
||||||
|
|
||||||
resp, body = self.request(url_to_use + url, method,
|
|
||||||
**kwargs)
|
|
||||||
return resp, body
|
|
||||||
|
|
||||||
def get(self, url, **kwargs):
|
def get(self, url, **kwargs):
|
||||||
return self._cs_request(url, 'GET', **kwargs)
|
return self._cs_request(url, 'GET', **kwargs)
|
||||||
|
@@ -37,14 +37,18 @@ class Session(object):
|
|||||||
REDIRECT_STATUSES = (301, 302, 303, 305, 307)
|
REDIRECT_STATUSES = (301, 302, 303, 305, 307)
|
||||||
DEFAULT_REDIRECT_LIMIT = 30
|
DEFAULT_REDIRECT_LIMIT = 30
|
||||||
|
|
||||||
def __init__(self, session=None, original_ip=None, verify=True, cert=None,
|
def __init__(self, auth=None, session=None, original_ip=None, verify=True,
|
||||||
timeout=None, user_agent=None,
|
cert=None, timeout=None, user_agent=None,
|
||||||
redirect=DEFAULT_REDIRECT_LIMIT):
|
redirect=DEFAULT_REDIRECT_LIMIT):
|
||||||
"""Maintains client communication state and common functionality.
|
"""Maintains client communication state and common functionality.
|
||||||
|
|
||||||
As much as possible the parameters to this class reflect and are passed
|
As much as possible the parameters to this class reflect and are passed
|
||||||
directly to the requests library.
|
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
|
:param string original_ip: The original IP of the requesting user
|
||||||
which will be sent to identity service in a
|
which will be sent to identity service in a
|
||||||
'Forwarded' header. (optional)
|
'Forwarded' header. (optional)
|
||||||
@@ -74,6 +78,7 @@ class Session(object):
|
|||||||
if not session:
|
if not session:
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
|
|
||||||
|
self.auth = auth
|
||||||
self.session = session
|
self.session = session
|
||||||
self.original_ip = original_ip
|
self.original_ip = original_ip
|
||||||
self.verify = verify
|
self.verify = verify
|
||||||
@@ -89,7 +94,8 @@ class Session(object):
|
|||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
|
|
||||||
def request(self, url, method, json=None, original_ip=None,
|
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.
|
"""Send an HTTP request with the specified characteristics.
|
||||||
|
|
||||||
Wrapper around `requests.Session.request` to handle tasks such as
|
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
|
can be followed by a request. Either an
|
||||||
integer for a specific count or True/False
|
integer for a specific count or True/False
|
||||||
for forever/never. (optional)
|
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
|
:param kwargs: any other parameter that can be passed to
|
||||||
requests.Session.request (such as `headers`). Except:
|
requests.Session.request (such as `headers`). Except:
|
||||||
'data' will be overwritten by the data in 'json' param.
|
'data' will be overwritten by the data in 'json' param.
|
||||||
@@ -125,6 +135,17 @@ class Session(object):
|
|||||||
|
|
||||||
headers = kwargs.setdefault('headers', dict())
|
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:
|
if self.cert:
|
||||||
kwargs.setdefault('cert', self.cert)
|
kwargs.setdefault('cert', self.cert)
|
||||||
|
|
||||||
@@ -286,3 +307,10 @@ class Session(object):
|
|||||||
session=kwargs.pop('session', None),
|
session=kwargs.pop('session', None),
|
||||||
original_ip=kwargs.pop('original_ip', None),
|
original_ip=kwargs.pop('original_ip', None),
|
||||||
user_agent=kwargs.pop('user_agent', 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)
|
||||||
|
0
keystoneclient/tests/auth/__init__.py
Normal file
0
keystoneclient/tests/auth/__init__.py
Normal file
@@ -17,6 +17,7 @@ import httpretty
|
|||||||
import mock
|
import mock
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from keystoneclient.auth import base
|
||||||
from keystoneclient import exceptions
|
from keystoneclient import exceptions
|
||||||
from keystoneclient import session as client_session
|
from keystoneclient import session as client_session
|
||||||
from keystoneclient.tests import utils
|
from keystoneclient.tests import utils
|
||||||
@@ -252,3 +253,47 @@ class ConstructSessionFromArgsTests(utils.TestCase):
|
|||||||
args = {key: value}
|
args = {key: value}
|
||||||
self.assertEqual(getattr(self._s(args), key), value)
|
self.assertEqual(getattr(self._s(args), key), value)
|
||||||
self.assertNotIn(key, args)
|
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)
|
||||||
|
Reference in New Issue
Block a user