summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2014-09-11 04:47:07 +0000
committerGerrit Code Review <review@openstack.org>2014-09-11 04:47:07 +0000
commitb580155f1cbbe614f388e0facbb5376bcc526d39 (patch)
treee03883cef143c605cc4e4bf2f5e9675d22afc56d
parentc846c616c5fb6da8ee1c519f375692d72b08b8e0 (diff)
parentd070347988e1fbc9f84439f1b63cb4d52a9bfcda (diff)
downloadpython-keystoneclient-b580155f1cbbe614f388e0facbb5376bcc526d39.tar.gz
Merge "Version independent plugins"
-rw-r--r--keystoneclient/auth/identity/base.py12
-rw-r--r--keystoneclient/auth/identity/generic/__init__.py21
-rw-r--r--keystoneclient/auth/identity/generic/base.py179
-rw-r--r--keystoneclient/auth/identity/generic/password.py83
-rw-r--r--keystoneclient/auth/identity/generic/token.py52
-rw-r--r--keystoneclient/tests/auth/test_password.py40
-rw-r--r--keystoneclient/tests/auth/test_token.py29
-rw-r--r--keystoneclient/tests/auth/utils.py114
-rw-r--r--setup.cfg3
9 files changed, 528 insertions, 5 deletions
diff --git a/keystoneclient/auth/identity/base.py b/keystoneclient/auth/identity/base.py
index 172655d..8069e5c 100644
--- a/keystoneclient/auth/identity/base.py
+++ b/keystoneclient/auth/identity/base.py
@@ -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
diff --git a/keystoneclient/auth/identity/generic/__init__.py b/keystoneclient/auth/identity/generic/__init__.py
new file mode 100644
index 0000000..b24c3d6
--- /dev/null
+++ b/keystoneclient/auth/identity/generic/__init__.py
@@ -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',
+ ]
diff --git a/keystoneclient/auth/identity/generic/base.py b/keystoneclient/auth/identity/generic/base.py
new file mode 100644
index 0000000..94d48ec
--- /dev/null
+++ b/keystoneclient/auth/identity/generic/base.py
@@ -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
diff --git a/keystoneclient/auth/identity/generic/password.py b/keystoneclient/auth/identity/generic/password.py
new file mode 100644
index 0000000..c8d9b7a
--- /dev/null
+++ b/keystoneclient/auth/identity/generic/password.py
@@ -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
diff --git a/keystoneclient/auth/identity/generic/token.py b/keystoneclient/auth/identity/generic/token.py
new file mode 100644
index 0000000..547ce36
--- /dev/null
+++ b/keystoneclient/auth/identity/generic/token.py
@@ -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
diff --git a/keystoneclient/tests/auth/test_password.py b/keystoneclient/tests/auth/test_password.py
new file mode 100644
index 0000000..5c93864
--- /dev/null
+++ b/keystoneclient/tests/auth/test_password.py
@@ -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)
diff --git a/keystoneclient/tests/auth/test_token.py b/keystoneclient/tests/auth/test_token.py
new file mode 100644
index 0000000..d9a11e0
--- /dev/null
+++ b/keystoneclient/tests/auth/test_token.py
@@ -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)
diff --git a/keystoneclient/tests/auth/utils.py b/keystoneclient/tests/auth/utils.py
index 5cc7011..c3dae8f 100644
--- a/keystoneclient/tests/auth/utils.py
+++ b/keystoneclient/tests/auth/utils.py
@@ -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()
diff --git a/setup.cfg b/setup.cfg
index ec8042d..db21d75 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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