Merge "Version independent plugins"
This commit is contained in:
@@ -24,6 +24,12 @@ from keystoneclient import utils
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_options():
|
||||
return [
|
||||
cfg.StrOpt('auth-url', help='Authentication URL'),
|
||||
]
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseIdentityPlugin(base.BaseAuthPlugin):
|
||||
|
||||
@@ -259,9 +265,5 @@ class BaseIdentityPlugin(base.BaseAuthPlugin):
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(BaseIdentityPlugin, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('auth-url', help='Authentication URL'),
|
||||
])
|
||||
|
||||
options.extend(get_options())
|
||||
return options
|
||||
|
21
keystoneclient/auth/identity/generic/__init__.py
Normal file
21
keystoneclient/auth/identity/generic/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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.
|
||||
|
||||
from keystoneclient.auth.identity.generic.base import BaseGenericPlugin # noqa
|
||||
from keystoneclient.auth.identity.generic.password import Password # noqa
|
||||
from keystoneclient.auth.identity.generic.token import Token # noqa
|
||||
|
||||
|
||||
__all__ = ['BaseGenericPlugin',
|
||||
'Password',
|
||||
'Token',
|
||||
]
|
179
keystoneclient/auth/identity/generic/base.py
Normal file
179
keystoneclient/auth/identity/generic/base.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# 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
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from keystoneclient import _discover
|
||||
from keystoneclient.auth.identity import base
|
||||
from keystoneclient import exceptions
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_options():
|
||||
return base.get_options() + [
|
||||
cfg.StrOpt('domain-id', help='Domain ID to scope to'),
|
||||
cfg.StrOpt('domain-name', help='Domain name to scope to'),
|
||||
cfg.StrOpt('tenant-id', help='Tenant ID to scope to'),
|
||||
cfg.StrOpt('tenant-name', help='Tenant name to scope to'),
|
||||
cfg.StrOpt('project-id', help='Project ID to scope to'),
|
||||
cfg.StrOpt('project-name', help='Project name to scope to'),
|
||||
cfg.StrOpt('project-domain-id',
|
||||
help='Domain ID containing project'),
|
||||
cfg.StrOpt('project-domain-name',
|
||||
help='Domain name containing project'),
|
||||
cfg.StrOpt('trust-id', help='Trust ID'),
|
||||
]
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseGenericPlugin(base.BaseIdentityPlugin):
|
||||
"""An identity plugin that is not version dependant.
|
||||
|
||||
Internally we will construct a version dependant plugin with the resolved
|
||||
URL and then proxy all calls from the base plugin to the versioned one.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_url,
|
||||
tenant_id=None,
|
||||
tenant_name=None,
|
||||
project_id=None,
|
||||
project_name=None,
|
||||
project_domain_id=None,
|
||||
project_domain_name=None,
|
||||
domain_id=None,
|
||||
domain_name=None,
|
||||
trust_id=None):
|
||||
super(BaseGenericPlugin, self).__init__(auth_url=auth_url)
|
||||
|
||||
self._project_id = project_id or tenant_id
|
||||
self._project_name = project_name or tenant_name
|
||||
self._project_domain_id = project_domain_id
|
||||
self._project_domain_name = project_domain_name
|
||||
self._domain_id = domain_id
|
||||
self._domain_name = domain_name
|
||||
self._trust_id = trust_id
|
||||
|
||||
self._plugin = None
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_plugin(self, session, version, url, raw_status=None):
|
||||
"""Create a plugin from the given paramters.
|
||||
|
||||
This function will be called multiple times with the version and url
|
||||
of a potential endpoint. If a plugin can be constructed that fits the
|
||||
params then it should return it. If not return None and then another
|
||||
call will be made with other available URLs.
|
||||
|
||||
:param Session session: A session object.
|
||||
:param tuple version: A tuple of the API version at the URL.
|
||||
:param string url: The base URL for this version.
|
||||
:param string raw_status: The status that was in the discovery field.
|
||||
|
||||
:returns: A plugin that can match the parameters or None if nothing.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def _has_domain_scope(self):
|
||||
"""Are there domain parameters.
|
||||
|
||||
Domain parameters are v3 only so returns if any are set.
|
||||
|
||||
:returns: True if a domain parameter is set, false otherwise.
|
||||
"""
|
||||
return any([self._domain_id, self._domain_name,
|
||||
self._project_domain_id, self._project_domain_name])
|
||||
|
||||
@property
|
||||
def _v2_params(self):
|
||||
"""Parameters that are common to v2 plugins."""
|
||||
return {'trust_id': self._trust_id,
|
||||
'tenant_id': self._project_id,
|
||||
'tenant_name': self._project_name}
|
||||
|
||||
@property
|
||||
def _v3_params(self):
|
||||
"""Parameters that are common to v3 plugins."""
|
||||
return {'trust_id': self._trust_id,
|
||||
'project_id': self._project_id,
|
||||
'project_name': self._project_name,
|
||||
'project_domain_id': self._project_domain_id,
|
||||
'project_domain_name': self._project_domain_name,
|
||||
'domain_id': self._domain_id,
|
||||
'domain_name': self._domain_name}
|
||||
|
||||
def _do_create_plugin(self, session):
|
||||
plugin = None
|
||||
|
||||
try:
|
||||
disc = self.get_discovery(session,
|
||||
self.auth_url,
|
||||
authenticated=False)
|
||||
except (exceptions.DiscoveryFailure,
|
||||
exceptions.HTTPError,
|
||||
exceptions.ConnectionError):
|
||||
LOG.warn('Discovering versions from the identity service failed '
|
||||
'when creating the password plugin. Attempting to '
|
||||
'determine version from URL.')
|
||||
|
||||
url_parts = urlparse.urlparse(self.auth_url)
|
||||
path = url_parts.path.lower()
|
||||
|
||||
if path.startswith('/v2.0') and not self._has_domain_scope:
|
||||
plugin = self.create_plugin(session, (2, 0), self.auth_url)
|
||||
elif path.startswith('/v3'):
|
||||
plugin = self.create_plugin(session, (3, 0), self.auth_url)
|
||||
|
||||
else:
|
||||
disc_data = disc.version_data()
|
||||
|
||||
for data in disc_data:
|
||||
version = data['version']
|
||||
|
||||
if (_discover.version_match((2,), version) and
|
||||
self._has_domain_scope):
|
||||
# NOTE(jamielennox): if there are domain parameters there
|
||||
# is no point even trying against v2 APIs.
|
||||
continue
|
||||
|
||||
plugin = self.create_plugin(session,
|
||||
version,
|
||||
data['url'],
|
||||
raw_status=data['raw_status'])
|
||||
|
||||
if plugin:
|
||||
break
|
||||
|
||||
if plugin:
|
||||
return plugin
|
||||
|
||||
# so there were no URLs that i could use for auth of any version.
|
||||
msg = 'Could not determine a suitable URL for the plugin'
|
||||
raise exceptions.DiscoveryFailure(msg)
|
||||
|
||||
def get_auth_ref(self, session, **kwargs):
|
||||
if not self._plugin:
|
||||
self._plugin = self._do_create_plugin(session)
|
||||
|
||||
return self._plugin.get_auth_ref(session, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(BaseGenericPlugin, cls).get_options()
|
||||
options.extend(get_options())
|
||||
return options
|
83
keystoneclient/auth/identity/generic/password.py
Normal file
83
keystoneclient/auth/identity/generic/password.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# 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
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from keystoneclient import _discover
|
||||
from keystoneclient.auth.identity.generic import base
|
||||
from keystoneclient.auth.identity import v2
|
||||
from keystoneclient.auth.identity import v3
|
||||
from keystoneclient import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_options():
|
||||
return [
|
||||
cfg.StrOpt('user-name', dest='username', help='Username',
|
||||
deprecated_name='username'),
|
||||
cfg.StrOpt('user-domain-id', help="User's domain id"),
|
||||
cfg.StrOpt('user-domain-name', help="User's domain name"),
|
||||
cfg.StrOpt('password', help="User's password"),
|
||||
]
|
||||
|
||||
|
||||
class Password(base.BaseGenericPlugin):
|
||||
"""A common user/password authentication plugin."""
|
||||
|
||||
@utils.positional()
|
||||
def __init__(self, auth_url, username=None, user_id=None, password=None,
|
||||
user_domain_id=None, user_domain_name=None, **kwargs):
|
||||
"""Construct plugin.
|
||||
|
||||
:param string username: Username for authentication.
|
||||
:param string user_id: User ID for authentication.
|
||||
:param string password: Password for authentication.
|
||||
:param string user_domain_id: User's domain ID for authentication.
|
||||
:param string user_domain_name: User's domain name for authentication.
|
||||
"""
|
||||
super(Password, self).__init__(auth_url=auth_url, **kwargs)
|
||||
|
||||
self._username = username
|
||||
self._user_id = user_id
|
||||
self._password = password
|
||||
self._user_domain_id = user_domain_id
|
||||
self._user_domain_name = user_domain_name
|
||||
|
||||
def create_plugin(self, session, version, url, raw_status=None):
|
||||
if _discover.version_match((2,), version):
|
||||
if self._user_domain_id or self._user_domain_name:
|
||||
# If you specify any domain parameters it won't work so quit.
|
||||
return None
|
||||
|
||||
return v2.Password(auth_url=url,
|
||||
user_id=self._user_id,
|
||||
username=self._username,
|
||||
password=self._password,
|
||||
**self._v2_params)
|
||||
|
||||
elif _discover.version_match((3,), version):
|
||||
return v3.Password(auth_url=url,
|
||||
user_id=self._user_id,
|
||||
username=self._username,
|
||||
user_domain_id=self._user_domain_id,
|
||||
user_domain_name=self._user_domain_name,
|
||||
password=self._password,
|
||||
**self._v3_params)
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(Password, cls).get_options()
|
||||
options.extend(get_options())
|
||||
return options
|
52
keystoneclient/auth/identity/generic/token.py
Normal file
52
keystoneclient/auth/identity/generic/token.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# 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
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from keystoneclient import _discover
|
||||
from keystoneclient.auth.identity.generic import base
|
||||
from keystoneclient.auth.identity import v2
|
||||
from keystoneclient.auth.identity import v3
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_options():
|
||||
return [
|
||||
cfg.StrOpt('token', help='Token to authenticate with'),
|
||||
]
|
||||
|
||||
|
||||
class Token(base.BaseGenericPlugin):
|
||||
|
||||
def __init__(self, auth_url, token=None, **kwargs):
|
||||
"""Construct a plugin.
|
||||
|
||||
:param string token: Token for authentication.
|
||||
"""
|
||||
super(Token, self).__init__(auth_url, **kwargs)
|
||||
self._token = token
|
||||
|
||||
def create_plugin(self, session, version, url, raw_status=None):
|
||||
if _discover.version_match((2,), version):
|
||||
return v2.Token(url, self._token, **self._v2_params)
|
||||
|
||||
elif _discover.version_match((3,), version):
|
||||
return v3.Token(url, self._token, **self._v3_params)
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(Token, cls).get_options()
|
||||
options.extend(get_options())
|
||||
return options
|
40
keystoneclient/tests/auth/test_password.py
Normal file
40
keystoneclient/tests/auth/test_password.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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 uuid
|
||||
|
||||
from keystoneclient.auth.identity.generic import password
|
||||
from keystoneclient.auth.identity import v2
|
||||
from keystoneclient.auth.identity import v3
|
||||
from keystoneclient.tests.auth import utils
|
||||
|
||||
|
||||
class PasswordTests(utils.GenericPluginTestCase):
|
||||
|
||||
PLUGIN_CLASS = password.Password
|
||||
V2_PLUGIN_CLASS = v2.Password
|
||||
V3_PLUGIN_CLASS = v3.Password
|
||||
|
||||
def new_plugin(self, **kwargs):
|
||||
kwargs.setdefault('username', uuid.uuid4().hex)
|
||||
kwargs.setdefault('password', uuid.uuid4().hex)
|
||||
return super(PasswordTests, self).new_plugin(**kwargs)
|
||||
|
||||
def test_with_user_domain_params(self):
|
||||
self.stub_discovery()
|
||||
|
||||
self.assertCreateV3(domain_id=uuid.uuid4().hex,
|
||||
user_domain_id=uuid.uuid4().hex)
|
||||
|
||||
def test_v3_user_params_v2_url(self):
|
||||
self.stub_discovery(v3=False)
|
||||
self.assertDiscoveryFailure(user_domain_id=uuid.uuid4().hex)
|
29
keystoneclient/tests/auth/test_token.py
Normal file
29
keystoneclient/tests/auth/test_token.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# 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 uuid
|
||||
|
||||
from keystoneclient.auth.identity.generic import token
|
||||
from keystoneclient.auth.identity import v2
|
||||
from keystoneclient.auth.identity import v3
|
||||
from keystoneclient.tests.auth import utils
|
||||
|
||||
|
||||
class TokenTests(utils.GenericPluginTestCase):
|
||||
|
||||
PLUGIN_CLASS = token.Token
|
||||
V2_PLUGIN_CLASS = v2.Token
|
||||
V3_PLUGIN_CLASS = v3.Token
|
||||
|
||||
def new_plugin(self, **kwargs):
|
||||
kwargs.setdefault('token', uuid.uuid4().hex)
|
||||
return super(TokenTests, self).new_plugin(**kwargs)
|
@@ -11,12 +11,17 @@
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
|
||||
from keystoneclient import access
|
||||
from keystoneclient.auth import base
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient import fixture
|
||||
from keystoneclient import session
|
||||
from keystoneclient.tests import utils
|
||||
|
||||
|
||||
@@ -81,3 +86,112 @@ class TestCase(utils.TestCase):
|
||||
def assertTestVals(self, plugin, vals=TEST_VALS):
|
||||
for k, v in six.iteritems(vals):
|
||||
self.assertEqual(v, plugin[k])
|
||||
|
||||
|
||||
class GenericPluginTestCase(utils.TestCase):
|
||||
|
||||
TEST_URL = 'http://keystone.host:5000/'
|
||||
|
||||
# OVERRIDE THESE IN SUB CLASSES
|
||||
PLUGIN_CLASS = None
|
||||
V2_PLUGIN_CLASS = None
|
||||
V3_PLUGIN_CLASS = None
|
||||
|
||||
def setUp(self):
|
||||
super(GenericPluginTestCase, self).setUp()
|
||||
|
||||
self.token_v2 = fixture.V2Token()
|
||||
self.token_v3 = fixture.V3Token()
|
||||
self.token_v3_id = uuid.uuid4().hex
|
||||
self.session = session.Session()
|
||||
|
||||
self.stub_url('POST', ['v2.0', 'tokens'], json=self.token_v2)
|
||||
self.stub_url('POST', ['v3', 'auth', 'tokens'],
|
||||
headers={'X-Subject-Token': self.token_v3_id},
|
||||
json=self.token_v3)
|
||||
|
||||
def new_plugin(self, **kwargs):
|
||||
kwargs.setdefault('auth_url', self.TEST_URL)
|
||||
return self.PLUGIN_CLASS(**kwargs)
|
||||
|
||||
def stub_discovery(self, base_url=None, **kwargs):
|
||||
kwargs.setdefault('href', self.TEST_URL)
|
||||
disc = fixture.DiscoveryList(**kwargs)
|
||||
self.stub_url('GET', json=disc, base_url=base_url, status_code=300)
|
||||
return disc
|
||||
|
||||
def assertCreateV3(self, **kwargs):
|
||||
auth = self.new_plugin(**kwargs)
|
||||
auth_ref = auth.get_auth_ref(self.session)
|
||||
self.assertIsInstance(auth_ref, access.AccessInfoV3)
|
||||
self.assertEqual(self.TEST_URL + 'v3/auth/tokens',
|
||||
self.requests.last_request.url)
|
||||
self.assertIsInstance(auth._plugin, self.V3_PLUGIN_CLASS)
|
||||
return auth
|
||||
|
||||
def assertCreateV2(self, **kwargs):
|
||||
auth = self.new_plugin(**kwargs)
|
||||
auth_ref = auth.get_auth_ref(self.session)
|
||||
self.assertIsInstance(auth_ref, access.AccessInfoV2)
|
||||
self.assertEqual(self.TEST_URL + 'v2.0/tokens',
|
||||
self.requests.last_request.url)
|
||||
self.assertIsInstance(auth._plugin, self.V2_PLUGIN_CLASS)
|
||||
return auth
|
||||
|
||||
def assertDiscoveryFailure(self, **kwargs):
|
||||
plugin = self.new_plugin(**kwargs)
|
||||
self.assertRaises(exceptions.DiscoveryFailure,
|
||||
plugin.get_auth_ref,
|
||||
self.session)
|
||||
|
||||
def test_create_v3_if_domain_params(self):
|
||||
self.stub_discovery()
|
||||
|
||||
self.assertCreateV3(domain_id=uuid.uuid4().hex)
|
||||
self.assertCreateV3(domain_name=uuid.uuid4().hex)
|
||||
self.assertCreateV3(project_name=uuid.uuid4().hex,
|
||||
project_domain_name=uuid.uuid4().hex)
|
||||
self.assertCreateV3(project_name=uuid.uuid4().hex,
|
||||
project_domain_id=uuid.uuid4().hex)
|
||||
|
||||
def test_create_v2_if_no_domain_params(self):
|
||||
self.stub_discovery()
|
||||
self.assertCreateV2()
|
||||
self.assertCreateV2(project_id=uuid.uuid4().hex)
|
||||
self.assertCreateV2(project_name=uuid.uuid4().hex)
|
||||
self.assertCreateV2(tenant_id=uuid.uuid4().hex)
|
||||
self.assertCreateV2(tenant_name=uuid.uuid4().hex)
|
||||
|
||||
def test_v3_params_v2_url(self):
|
||||
self.stub_discovery(v3=False)
|
||||
self.assertDiscoveryFailure(domain_name=uuid.uuid4().hex)
|
||||
|
||||
def test_v2_params_v3_url(self):
|
||||
self.stub_discovery(v2=False)
|
||||
self.assertCreateV3()
|
||||
|
||||
def test_no_urls(self):
|
||||
self.stub_discovery(v2=False, v3=False)
|
||||
self.assertDiscoveryFailure()
|
||||
|
||||
def test_path_based_url_v2(self):
|
||||
self.stub_url('GET', ['v2.0'], status_code=403)
|
||||
self.assertCreateV2(auth_url=self.TEST_URL + 'v2.0')
|
||||
|
||||
def test_path_based_url_v3(self):
|
||||
self.stub_url('GET', ['v3'], status_code=403)
|
||||
self.assertCreateV3(auth_url=self.TEST_URL + 'v3')
|
||||
|
||||
def test_disc_error_for_failure(self):
|
||||
self.stub_url('GET', [], status_code=403)
|
||||
self.assertDiscoveryFailure()
|
||||
|
||||
def test_v3_plugin_from_failure(self):
|
||||
url = self.TEST_URL + 'v3'
|
||||
self.stub_url('GET', [], base_url=url, status_code=403)
|
||||
self.assertCreateV3(auth_url=url)
|
||||
|
||||
def test_unknown_discovery_version(self):
|
||||
# make a v4 entry that's mostly the same as a v3
|
||||
self.stub_discovery(v2=False, v3_id='v4.0')
|
||||
self.assertDiscoveryFailure()
|
||||
|
@@ -28,6 +28,8 @@ console_scripts =
|
||||
keystone = keystoneclient.shell:main
|
||||
|
||||
keystoneclient.auth.plugin =
|
||||
password = keystoneclient.auth.identity.generic:Password
|
||||
token = keystoneclient.auth.identity.generic:Token
|
||||
v2password = keystoneclient.auth.identity.v2:Password
|
||||
v2token = keystoneclient.auth.identity.v2:Token
|
||||
v3password = keystoneclient.auth.identity.v3:Password
|
||||
@@ -35,6 +37,7 @@ keystoneclient.auth.plugin =
|
||||
v3unscopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2UnscopedToken
|
||||
v3scopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2ScopedToken
|
||||
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
|
Reference in New Issue
Block a user