diff options
| author | Jamie Lennox <jamielennox@redhat.com> | 2015-02-11 19:03:25 +1100 |
|---|---|---|
| committer | Jamie Lennox <jamielennox@redhat.com> | 2015-02-11 19:03:25 +1100 |
| commit | 6bd93179a2966f2b5c67e297628510ac73689fb3 (patch) | |
| tree | faf3a93a16fb49b4a742f74b6fcdd20f8a0ebd0e /keystoneclient/tests/unit | |
| parent | 58ac2de5d4a6b58e8bd5d430a04199a4d40427a8 (diff) | |
| download | python-keystoneclient-6bd93179a2966f2b5c67e297628510ac73689fb3.tar.gz | |
Move tests to the unit subdirectory
Move all the existing tests to the unit/ subdirectory. This gives us
some room to add a functional/ directory later with other tests.
Change-Id: I0fb8d5b628eb8ee1f35f05f42d0c0ac9f285e8c3
Implements: functional-testing
Diffstat (limited to 'keystoneclient/tests/unit')
81 files changed, 17444 insertions, 0 deletions
diff --git a/keystoneclient/tests/unit/__init__.py b/keystoneclient/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/keystoneclient/tests/unit/__init__.py diff --git a/keystoneclient/tests/unit/apiclient/__init__.py b/keystoneclient/tests/unit/apiclient/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/keystoneclient/tests/unit/apiclient/__init__.py diff --git a/keystoneclient/tests/unit/apiclient/test_exceptions.py b/keystoneclient/tests/unit/apiclient/test_exceptions.py new file mode 100644 index 0000000..4a803c7 --- /dev/null +++ b/keystoneclient/tests/unit/apiclient/test_exceptions.py @@ -0,0 +1,68 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# 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 six + +from keystoneclient import exceptions +from keystoneclient.tests.unit import utils + + +class FakeResponse(object): + json_data = {} + + def __init__(self, **kwargs): + for key, value in six.iteritems(kwargs): + setattr(self, key, value) + + def json(self): + return self.json_data + + +class ExceptionsArgsTest(utils.TestCase): + + def assert_exception(self, ex_cls, method, url, status_code, json_data): + ex = exceptions.from_response( + FakeResponse(status_code=status_code, + headers={"Content-Type": "application/json"}, + json_data=json_data), + method, + url) + self.assertIsInstance(ex, ex_cls) + self.assertEqual(ex.message, json_data["error"]["message"]) + self.assertEqual(ex.details, json_data["error"]["details"]) + self.assertEqual(ex.method, method) + self.assertEqual(ex.url, url) + self.assertEqual(ex.http_status, status_code) + + def test_from_response_known(self): + method = "GET" + url = "/fake" + status_code = 400 + json_data = {"error": {"message": "fake message", + "details": "fake details"}} + self.assert_exception( + exceptions.BadRequest, method, url, status_code, json_data) + + def test_from_response_unknown(self): + method = "POST" + url = "/fake-unknown" + status_code = 499 + json_data = {"error": {"message": "fake unknown message", + "details": "fake unknown details"}} + self.assert_exception( + exceptions.HTTPClientError, method, url, status_code, json_data) + status_code = 600 + self.assert_exception( + exceptions.HTTPError, method, url, status_code, json_data) diff --git a/keystoneclient/tests/unit/auth/__init__.py b/keystoneclient/tests/unit/auth/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/keystoneclient/tests/unit/auth/__init__.py diff --git a/keystoneclient/tests/unit/auth/test_access.py b/keystoneclient/tests/unit/auth/test_access.py new file mode 100644 index 0000000..405fb8b --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_access.py @@ -0,0 +1,61 @@ +# 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 import access +from keystoneclient import auth +from keystoneclient.auth.identity import access as access_plugin +from keystoneclient import fixture +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class AccessInfoPluginTests(utils.TestCase): + + def setUp(self): + super(AccessInfoPluginTests, self).setUp() + self.session = session.Session() + self.auth_token = uuid.uuid4().hex + + def _plugin(self, **kwargs): + token = fixture.V3Token() + s = token.add_service('identity') + s.add_standard_endpoints(public=self.TEST_ROOT_URL) + + auth_ref = access.AccessInfo.factory(body=token, + auth_token=self.auth_token) + return access_plugin.AccessInfoPlugin(auth_ref, **kwargs) + + def test_auth_ref(self): + plugin = self._plugin() + self.assertEqual(self.TEST_ROOT_URL, + plugin.get_endpoint(self.session, + service_type='identity', + interface='public')) + self.assertEqual(self.auth_token, plugin.get_token(session)) + + def test_auth_url(self): + auth_url = 'http://keystone.test.url' + plugin = self._plugin(auth_url=auth_url) + + self.assertEqual(auth_url, + plugin.get_endpoint(self.session, + interface=auth.AUTH_INTERFACE)) + + def test_invalidate(self): + plugin = self._plugin() + auth_ref = plugin.auth_ref + + self.assertIsInstance(auth_ref, access.AccessInfo) + self.assertFalse(plugin.invalidate()) + self.assertIs(auth_ref, plugin.auth_ref) diff --git a/keystoneclient/tests/unit/auth/test_cli.py b/keystoneclient/tests/unit/auth/test_cli.py new file mode 100644 index 0000000..d65de73 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_cli.py @@ -0,0 +1,196 @@ +# 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 argparse +import uuid + +import fixtures +import mock +from oslo_config import cfg + +from keystoneclient.auth import base +from keystoneclient.auth import cli +from keystoneclient.tests.unit.auth import utils + + +class TesterPlugin(base.BaseAuthPlugin): + + def get_token(self, *args, **kwargs): + return None + + @classmethod + def get_options(cls): + # NOTE(jamielennox): this is kind of horrible. If you specify this as + # a deprecated_name= value it will convert - to _ which is not what we + # want for a CLI option. + deprecated = [cfg.DeprecatedOpt('test-other')] + return [ + cfg.StrOpt('test-opt', help='tester', deprecated_opts=deprecated) + ] + + +class CliTests(utils.TestCase): + + def setUp(self): + super(CliTests, self).setUp() + self.p = argparse.ArgumentParser() + + def env(self, name, value=None): + if value is not None: + # environment variables are always strings + value = str(value) + + return self.useFixture(fixtures.EnvironmentVariable(name, value)) + + def test_creating_with_no_args(self): + ret = cli.register_argparse_arguments(self.p, []) + self.assertIsNone(ret) + self.assertIn('--os-auth-plugin', self.p.format_usage()) + + def test_load_with_nothing(self): + cli.register_argparse_arguments(self.p, []) + opts = self.p.parse_args([]) + self.assertIsNone(cli.load_from_argparse_arguments(opts)) + + @utils.mock_plugin + def test_basic_params_added(self, m): + name = uuid.uuid4().hex + argv = ['--os-auth-plugin', name] + ret = cli.register_argparse_arguments(self.p, argv) + self.assertIs(utils.MockPlugin, ret) + + for n in ('--os-a-int', '--os-a-bool', '--os-a-float'): + self.assertIn(n, self.p.format_usage()) + + m.assert_called_once_with(name) + + @utils.mock_plugin + def test_param_loading(self, m): + name = uuid.uuid4().hex + argv = ['--os-auth-plugin', name, + '--os-a-int', str(self.a_int), + '--os-a-float', str(self.a_float), + '--os-a-bool', str(self.a_bool)] + + klass = cli.register_argparse_arguments(self.p, argv) + self.assertIs(utils.MockPlugin, klass) + + opts = self.p.parse_args(argv) + self.assertEqual(name, opts.os_auth_plugin) + + a = cli.load_from_argparse_arguments(opts) + self.assertTestVals(a) + + self.assertEqual(name, opts.os_auth_plugin) + self.assertEqual(str(self.a_int), opts.os_a_int) + self.assertEqual(str(self.a_float), opts.os_a_float) + self.assertEqual(str(self.a_bool), opts.os_a_bool) + + @utils.mock_plugin + def test_default_options(self, m): + name = uuid.uuid4().hex + argv = ['--os-auth-plugin', name, + '--os-a-float', str(self.a_float)] + + klass = cli.register_argparse_arguments(self.p, argv) + self.assertIs(utils.MockPlugin, klass) + + opts = self.p.parse_args(argv) + self.assertEqual(name, opts.os_auth_plugin) + + a = cli.load_from_argparse_arguments(opts) + + self.assertEqual(self.a_float, a['a_float']) + self.assertEqual(3, a['a_int']) + + @utils.mock_plugin + def test_with_default_string_value(self, m): + name = uuid.uuid4().hex + klass = cli.register_argparse_arguments(self.p, [], default=name) + self.assertIs(utils.MockPlugin, klass) + m.assert_called_once_with(name) + + @utils.mock_plugin + def test_overrides_default_string_value(self, m): + name = uuid.uuid4().hex + default = uuid.uuid4().hex + argv = ['--os-auth-plugin', name] + klass = cli.register_argparse_arguments(self.p, argv, default=default) + self.assertIs(utils.MockPlugin, klass) + m.assert_called_once_with(name) + + @utils.mock_plugin + def test_with_default_type_value(self, m): + klass = cli.register_argparse_arguments(self.p, [], + default=utils.MockPlugin) + self.assertIs(utils.MockPlugin, klass) + self.assertEqual(0, m.call_count) + + @utils.mock_plugin + def test_overrides_default_type_value(self, m): + # using this test plugin would fail if called because there + # is no get_options() function + class TestPlugin(object): + pass + name = uuid.uuid4().hex + argv = ['--os-auth-plugin', name] + klass = cli.register_argparse_arguments(self.p, argv, + default=TestPlugin) + self.assertIs(utils.MockPlugin, klass) + m.assert_called_once_with(name) + + @utils.mock_plugin + def test_env_overrides_default_opt(self, m): + name = uuid.uuid4().hex + val = uuid.uuid4().hex + self.env('OS_A_STR', val) + + klass = cli.register_argparse_arguments(self.p, [], default=name) + opts = self.p.parse_args([]) + a = klass.load_from_argparse_arguments(opts) + + self.assertEqual(val, a['a_str']) + + def test_deprecated_cli_options(self): + TesterPlugin.register_argparse_arguments(self.p) + val = uuid.uuid4().hex + opts = self.p.parse_args(['--os-test-other', val]) + self.assertEqual(val, opts.os_test_opt) + + def test_deprecated_multi_cli_options(self): + TesterPlugin.register_argparse_arguments(self.p) + val1 = uuid.uuid4().hex + val2 = uuid.uuid4().hex + # argarse rules say that the last specified wins. + opts = self.p.parse_args(['--os-test-other', val2, + '--os-test-opt', val1]) + self.assertEqual(val1, opts.os_test_opt) + + def test_deprecated_env_options(self): + val = uuid.uuid4().hex + + with mock.patch.dict('os.environ', {'OS_TEST_OTHER': val}): + TesterPlugin.register_argparse_arguments(self.p) + + opts = self.p.parse_args([]) + self.assertEqual(val, opts.os_test_opt) + + def test_deprecated_env_multi_options(self): + val1 = uuid.uuid4().hex + val2 = uuid.uuid4().hex + + with mock.patch.dict('os.environ', {'OS_TEST_OPT': val1, + 'OS_TEST_OTHER': val2}): + TesterPlugin.register_argparse_arguments(self.p) + + opts = self.p.parse_args([]) + self.assertEqual(val1, opts.os_test_opt) diff --git a/keystoneclient/tests/unit/auth/test_conf.py b/keystoneclient/tests/unit/auth/test_conf.py new file mode 100644 index 0000000..c3ce8eb --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_conf.py @@ -0,0 +1,177 @@ +# 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 + +import mock +from oslo_config import cfg +from oslo_config import fixture as config +import stevedore + +from keystoneclient.auth import base +from keystoneclient.auth import conf +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient import exceptions +from keystoneclient.tests.unit.auth import utils + + +class ConfTests(utils.TestCase): + + def setUp(self): + super(ConfTests, self).setUp() + self.conf_fixture = self.useFixture(config.Config()) + + # NOTE(jamielennox): we register the basic config options first because + # we need them in place before we can stub them. We will need to run + # the register again after we stub the auth section and auth plugin so + # it can load the plugin specific options. + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + def test_loading_v2(self): + section = uuid.uuid4().hex + username = uuid.uuid4().hex + password = uuid.uuid4().hex + trust_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(v2_auth.Password.get_options(), + group=section) + + self.conf_fixture.config(auth_plugin=self.V2PASS, + username=username, + password=password, + trust_id=trust_id, + tenant_id=tenant_id, + group=section) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + + self.assertEqual(username, a.username) + self.assertEqual(password, a.password) + self.assertEqual(trust_id, a.trust_id) + self.assertEqual(tenant_id, a.tenant_id) + + def test_loading_v3(self): + section = uuid.uuid4().hex + token = uuid.uuid4().hex + trust_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + project_domain_name = uuid.uuid4().hex + + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(v3_auth.Token.get_options(), + group=section) + + self.conf_fixture.config(auth_plugin=self.V3TOKEN, + token=token, + trust_id=trust_id, + project_id=project_id, + project_domain_name=project_domain_name, + group=section) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + + self.assertEqual(token, a.auth_methods[0].token) + self.assertEqual(trust_id, a.trust_id) + self.assertEqual(project_id, a.project_id) + self.assertEqual(project_domain_name, a.project_domain_name) + + def test_loading_invalid_plugin(self): + auth_plugin = uuid.uuid4().hex + self.conf_fixture.config(auth_plugin=auth_plugin, + group=self.GROUP) + + e = self.assertRaises(exceptions.NoMatchingPlugin, + conf.load_from_conf_options, + self.conf_fixture.conf, + self.GROUP) + + self.assertEqual(auth_plugin, e.name) + + def test_loading_with_no_data(self): + self.assertIsNone(conf.load_from_conf_options(self.conf_fixture.conf, + self.GROUP)) + + @mock.patch('stevedore.DriverManager') + def test_other_params(self, m): + m.return_value = utils.MockManager(utils.MockPlugin) + driver_name = uuid.uuid4().hex + + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=self.GROUP) + self.conf_fixture.config(auth_plugin=driver_name, + group=self.GROUP, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + m.assert_called_once_with(namespace=base.PLUGIN_NAMESPACE, + name=driver_name, + invoke_on_load=False) + + @utils.mock_plugin + def test_same_section(self, m): + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + self.conf_fixture.config(auth_plugin=uuid.uuid4().hex, + group=self.GROUP, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + @utils.mock_plugin + def test_diff_section(self, m): + section = uuid.uuid4().hex + + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=section) + self.conf_fixture.config(group=section, + auth_plugin=uuid.uuid4().hex, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + def test_plugins_are_all_opts(self): + manager = stevedore.ExtensionManager(base.PLUGIN_NAMESPACE, + invoke_on_load=False, + propagate_map_exceptions=True) + + def inner(driver): + for p in driver.plugin.get_options(): + self.assertIsInstance(p, cfg.Opt) + + manager.map(inner) + + def test_get_common(self): + opts = conf.get_common_conf_options() + for opt in opts: + self.assertIsInstance(opt, cfg.Opt) + self.assertEqual(2, len(opts)) + + def test_get_named(self): + loaded_opts = conf.get_plugin_options('v2password') + plugin_opts = v2_auth.Password.get_options() + + self.assertEqual(plugin_opts, loaded_opts) diff --git a/keystoneclient/tests/unit/auth/test_identity_common.py b/keystoneclient/tests/unit/auth/test_identity_common.py new file mode 100644 index 0000000..db30bea --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_identity_common.py @@ -0,0 +1,422 @@ +# 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 datetime +import uuid + +from oslo_utils import timeutils +import six + +from keystoneclient import access +from keystoneclient.auth import base +from keystoneclient.auth import identity +from keystoneclient import fixture +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +@six.add_metaclass(abc.ABCMeta) +class CommonIdentityTests(object): + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + + TEST_COMPUTE_PUBLIC = 'http://nova/novapi/public' + TEST_COMPUTE_INTERNAL = 'http://nova/novapi/internal' + TEST_COMPUTE_ADMIN = 'http://nova/novapi/admin' + + TEST_PASS = uuid.uuid4().hex + + def setUp(self): + super(CommonIdentityTests, self).setUp() + + self.TEST_URL = '%s%s' % (self.TEST_ROOT_URL, self.version) + self.TEST_ADMIN_URL = '%s%s' % (self.TEST_ROOT_ADMIN_URL, self.version) + self.TEST_DISCOVERY = fixture.DiscoveryList(href=self.TEST_ROOT_URL) + + self.stub_auth_data() + + @abc.abstractmethod + def create_auth_plugin(self, **kwargs): + """Create an auth plugin that makes sense for the auth data. + + It doesn't really matter what auth mechanism is used but it should be + appropriate to the API version. + """ + + @abc.abstractmethod + def get_auth_data(self, **kwargs): + """Return fake authentication data. + + This should register a valid token response and ensure that the compute + endpoints are set to TEST_COMPUTE_PUBLIC, _INTERNAL and _ADMIN. + """ + + def stub_auth_data(self, **kwargs): + token = self.get_auth_data(**kwargs) + self.user_id = token.user_id + + try: + self.project_id = token.project_id + except AttributeError: + self.project_id = token.tenant_id + + self.stub_auth(json=token) + + @abc.abstractproperty + def version(self): + """The API version being tested.""" + + def test_discovering(self): + self.stub_url('GET', [], + base_url=self.TEST_COMPUTE_ADMIN, + json=self.TEST_DISCOVERY) + + body = 'SUCCESS' + + # which gives our sample values + self.stub_url('GET', ['path'], text=body) + + a = self.create_auth_plugin() + s = session.Session(auth=a) + + resp = s.get('/path', endpoint_filter={'service_type': 'compute', + 'interface': 'admin', + 'version': self.version}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(body, resp.text) + + new_body = 'SC SUCCESS' + # if we don't specify a version, we use the URL from the SC + self.stub_url('GET', ['path'], + base_url=self.TEST_COMPUTE_ADMIN, + text=new_body) + + resp = s.get('/path', endpoint_filter={'service_type': 'compute', + 'interface': 'admin'}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(new_body, resp.text) + + def test_discovery_uses_session_cache(self): + # register responses such that if the discovery URL is hit more than + # once then the response will be invalid and not point to COMPUTE_ADMIN + resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}] + self.requests.get(self.TEST_COMPUTE_ADMIN, resps) + + body = 'SUCCESS' + self.stub_url('GET', ['path'], text=body) + + # now either of the two plugins I use, it should not cause a second + # request to the discovery url. + s = session.Session() + a = self.create_auth_plugin() + b = self.create_auth_plugin() + + for auth in (a, b): + resp = s.get('/path', + auth=auth, + endpoint_filter={'service_type': 'compute', + 'interface': 'admin', + 'version': self.version}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(body, resp.text) + + def test_discovery_uses_plugin_cache(self): + # register responses such that if the discovery URL is hit more than + # once then the response will be invalid and not point to COMPUTE_ADMIN + resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}] + self.requests.get(self.TEST_COMPUTE_ADMIN, resps) + + body = 'SUCCESS' + self.stub_url('GET', ['path'], text=body) + + # now either of the two sessions I use, it should not cause a second + # request to the discovery url. + sa = session.Session() + sb = session.Session() + auth = self.create_auth_plugin() + + for sess in (sa, sb): + resp = sess.get('/path', + auth=auth, + endpoint_filter={'service_type': 'compute', + 'interface': 'admin', + 'version': self.version}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(body, resp.text) + + def test_discovering_with_no_data(self): + # which returns discovery information pointing to TEST_URL but there is + # no data there. + self.stub_url('GET', [], + base_url=self.TEST_COMPUTE_ADMIN, + status_code=400) + + # so the url that will be used is the same TEST_COMPUTE_ADMIN + body = 'SUCCESS' + self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, + text=body, status_code=200) + + a = self.create_auth_plugin() + s = session.Session(auth=a) + + resp = s.get('/path', endpoint_filter={'service_type': 'compute', + 'interface': 'admin', + 'version': self.version}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(body, resp.text) + + def test_asking_for_auth_endpoint_ignores_checks(self): + a = self.create_auth_plugin() + s = session.Session(auth=a) + + auth_url = s.get_endpoint(service_type='compute', + interface=base.AUTH_INTERFACE) + + self.assertEqual(self.TEST_URL, auth_url) + + def _create_expired_auth_plugin(self, **kwargs): + expires = timeutils.utcnow() - datetime.timedelta(minutes=20) + expired_token = self.get_auth_data(expires=expires) + expired_auth_ref = access.AccessInfo.factory(body=expired_token) + + body = 'SUCCESS' + self.stub_url('GET', ['path'], + base_url=self.TEST_COMPUTE_ADMIN, text=body) + + a = self.create_auth_plugin(**kwargs) + a.auth_ref = expired_auth_ref + return a + + def test_reauthenticate(self): + a = self._create_expired_auth_plugin() + expired_auth_ref = a.auth_ref + s = session.Session(auth=a) + self.assertIsNot(expired_auth_ref, a.get_access(s)) + + def test_no_reauthenticate(self): + a = self._create_expired_auth_plugin(reauthenticate=False) + expired_auth_ref = a.auth_ref + s = session.Session(auth=a) + self.assertIs(expired_auth_ref, a.get_access(s)) + + def test_invalidate(self): + a = self.create_auth_plugin() + s = session.Session(auth=a) + + # trigger token fetching + s.get_auth_headers() + + self.assertTrue(a.auth_ref) + self.assertTrue(a.invalidate()) + self.assertIsNone(a.auth_ref) + self.assertFalse(a.invalidate()) + + def test_get_auth_properties(self): + a = self.create_auth_plugin() + s = session.Session() + + self.assertEqual(self.user_id, a.get_user_id(s)) + self.assertEqual(self.project_id, a.get_project_id(s)) + + +class V3(CommonIdentityTests, utils.TestCase): + + @property + def version(self): + return 'v3' + + def get_auth_data(self, **kwargs): + token = fixture.V3Token(**kwargs) + region = 'RegionOne' + + svc = token.add_service('identity') + svc.add_standard_endpoints(admin=self.TEST_ADMIN_URL, region=region) + + svc = token.add_service('compute') + svc.add_standard_endpoints(admin=self.TEST_COMPUTE_ADMIN, + public=self.TEST_COMPUTE_PUBLIC, + internal=self.TEST_COMPUTE_INTERNAL, + region=region) + + return token + + def stub_auth(self, subject_token=None, **kwargs): + if not subject_token: + subject_token = self.TEST_TOKEN + + kwargs.setdefault('headers', {})['X-Subject-Token'] = subject_token + self.stub_url('POST', ['auth', 'tokens'], **kwargs) + + def create_auth_plugin(self, **kwargs): + kwargs.setdefault('auth_url', self.TEST_URL) + kwargs.setdefault('username', self.TEST_USER) + kwargs.setdefault('password', self.TEST_PASS) + return identity.V3Password(**kwargs) + + +class V2(CommonIdentityTests, utils.TestCase): + + @property + def version(self): + return 'v2.0' + + def create_auth_plugin(self, **kwargs): + kwargs.setdefault('auth_url', self.TEST_URL) + kwargs.setdefault('username', self.TEST_USER) + kwargs.setdefault('password', self.TEST_PASS) + return identity.V2Password(**kwargs) + + def get_auth_data(self, **kwargs): + token = fixture.V2Token(**kwargs) + region = 'RegionOne' + + svc = token.add_service('identity') + svc.add_endpoint(self.TEST_ADMIN_URL, region=region) + + svc = token.add_service('compute') + svc.add_endpoint(public=self.TEST_COMPUTE_PUBLIC, + internal=self.TEST_COMPUTE_INTERNAL, + admin=self.TEST_COMPUTE_ADMIN, + region=region) + + return token + + def stub_auth(self, **kwargs): + self.stub_url('POST', ['tokens'], **kwargs) + + +class CatalogHackTests(utils.TestCase): + + TEST_URL = 'http://keystone.server:5000/v2.0' + OTHER_URL = 'http://other.server:5000/path' + + IDENTITY = 'identity' + + BASE_URL = 'http://keystone.server:5000/' + V2_URL = BASE_URL + 'v2.0' + V3_URL = BASE_URL + 'v3' + + def test_getting_endpoints(self): + disc = fixture.DiscoveryList(href=self.BASE_URL) + self.stub_url('GET', + ['/'], + base_url=self.BASE_URL, + json=disc) + + token = fixture.V2Token() + service = token.add_service(self.IDENTITY) + service.add_endpoint(public=self.V2_URL, + admin=self.V2_URL, + internal=self.V2_URL) + + self.stub_url('POST', + ['tokens'], + base_url=self.V2_URL, + json=token) + + v2_auth = identity.V2Password(self.V2_URL, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + sess = session.Session(auth=v2_auth) + + endpoint = sess.get_endpoint(service_type=self.IDENTITY, + interface='public', + version=(3, 0)) + + self.assertEqual(self.V3_URL, endpoint) + + def test_returns_original_when_discover_fails(self): + token = fixture.V2Token() + service = token.add_service(self.IDENTITY) + service.add_endpoint(public=self.V2_URL, + admin=self.V2_URL, + internal=self.V2_URL) + + self.stub_url('POST', + ['tokens'], + base_url=self.V2_URL, + json=token) + + self.stub_url('GET', [], base_url=self.BASE_URL, status_code=404) + + v2_auth = identity.V2Password(self.V2_URL, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + sess = session.Session(auth=v2_auth) + + endpoint = sess.get_endpoint(service_type=self.IDENTITY, + interface='public', + version=(3, 0)) + + self.assertEqual(self.V2_URL, endpoint) + + +class GenericPlugin(base.BaseAuthPlugin): + + BAD_TOKEN = uuid.uuid4().hex + + def __init__(self): + super(GenericPlugin, self).__init__() + + self.endpoint = 'http://keystone.host:5000' + + self.headers = {'headerA': 'valueA', + 'headerB': 'valueB'} + + def url(self, prefix): + return '%s/%s' % (self.endpoint, prefix) + + def get_token(self, session, **kwargs): + # NOTE(jamielennox): by specifying get_headers this should not be used + return self.BAD_TOKEN + + def get_headers(self, session, **kwargs): + return self.headers + + def get_endpoint(self, session, **kwargs): + return self.endpoint + + +class GenericAuthPluginTests(utils.TestCase): + + # filter doesn't matter to GenericPlugin, but we have to specify one + ENDPOINT_FILTER = {uuid.uuid4().hex: uuid.uuid4().hex} + + def setUp(self): + super(GenericAuthPluginTests, self).setUp() + self.auth = GenericPlugin() + self.session = session.Session(auth=self.auth) + + def test_setting_headers(self): + text = uuid.uuid4().hex + self.stub_url('GET', base_url=self.auth.url('prefix'), text=text) + + resp = self.session.get('prefix', endpoint_filter=self.ENDPOINT_FILTER) + + self.assertEqual(text, resp.text) + + for k, v in six.iteritems(self.auth.headers): + self.assertRequestHeaderEqual(k, v) + + self.assertIsNone(self.session.get_token()) + self.assertEqual(self.auth.headers, + self.session.get_auth_headers()) + self.assertNotIn('X-Auth-Token', self.requests.last_request.headers) diff --git a/keystoneclient/tests/unit/auth/test_identity_v2.py b/keystoneclient/tests/unit/auth/test_identity_v2.py new file mode 100644 index 0000000..6d432a7 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_identity_v2.py @@ -0,0 +1,295 @@ +# 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 copy +import uuid + +from keystoneclient.auth.identity import v2 +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class V2IdentityPlugin(utils.TestCase): + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') + + TEST_PASS = 'password' + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "adminURL": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8774/v1.0", + "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" + }], + "type": "nova_compat", + "name": "nova_compat" + }, { + "endpoints": [{ + "adminURL": "http://nova/novapi/admin", + "region": "RegionOne", + "internalURL": "http://nova/novapi/internal", + "publicURL": "http://nova/novapi/public" + }], + "type": "compute", + "name": "nova" + }, { + "endpoints": [{ + "adminURL": "http://glance/glanceapi/admin", + "region": "RegionOne", + "internalURL": "http://glance/glanceapi/internal", + "publicURL": "http://glance/glanceapi/public" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "adminURL": TEST_ADMIN_URL, + "region": "RegionOne", + "internalURL": "http://127.0.0.1:5000/v2.0", + "publicURL": "http://127.0.0.1:5000/v2.0" + }], + "type": "identity", + "name": "keystone" + }, { + "endpoints": [{ + "adminURL": "http://swift/swiftapi/admin", + "region": "RegionOne", + "internalURL": "http://swift/swiftapi/internal", + "publicURL": "http://swift/swiftapi/public" + }], + "type": "object-store", + "name": "swift" + }] + + def setUp(self): + super(V2IdentityPlugin, self).setUp() + self.TEST_RESPONSE_DICT = { + "access": { + "token": { + "expires": "2020-01-01T00:00:10.000123Z", + "id": self.TEST_TOKEN, + "tenant": { + "id": self.TEST_TENANT_ID + }, + }, + "user": { + "id": self.TEST_USER + }, + "serviceCatalog": self.TEST_SERVICE_CATALOG, + }, + } + + def stub_auth(self, **kwargs): + self.stub_url('POST', ['tokens'], **kwargs) + + def test_authenticate_with_username_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + self.assertIsNone(a.user_id) + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, + 'password': self.TEST_PASS}}} + self.assertRequestBodyIs(json=req) + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_user_id_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, user_id=self.TEST_USER, + password=self.TEST_PASS) + self.assertIsNone(a.username) + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER, + 'password': self.TEST_PASS}}} + self.assertRequestBodyIs(json=req) + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_username_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID) + self.assertIsNone(a.user_id) + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, + 'password': self.TEST_PASS}, + 'tenantId': self.TEST_TENANT_ID}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_user_id_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, user_id=self.TEST_USER, + password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID) + self.assertIsNone(a.username) + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER, + 'password': self.TEST_PASS}, + 'tenantId': self.TEST_TENANT_ID}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_token(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Token(self.TEST_URL, 'foo') + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'token': {'id': 'foo'}}} + self.assertRequestBodyIs(json=req) + self.assertRequestHeaderEqual('x-Auth-Token', 'foo') + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_trust_id(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, trust_id='trust') + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, + 'password': self.TEST_PASS}, + 'trust_id': 'trust'}} + + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def _do_service_url_test(self, base_url, endpoint_filter): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', ['path'], + base_url=base_url, + text='SUCCESS', status_code=200) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + resp = s.get('/path', endpoint_filter=endpoint_filter) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(self.requests.last_request.url, base_url + '/path') + + def test_service_url(self): + endpoint_filter = {'service_type': 'compute', + 'interface': 'admin', + 'service_name': 'nova'} + self._do_service_url_test('http://nova/novapi/admin', endpoint_filter) + + def test_service_url_defaults_to_public(self): + endpoint_filter = {'service_type': 'compute'} + self._do_service_url_test('http://nova/novapi/public', endpoint_filter) + + def test_endpoint_filter_without_service_type_fails(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.EndpointNotFound, s.get, '/path', + endpoint_filter={'interface': 'admin'}) + + def test_full_url_overrides_endpoint_filter(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [], + base_url='http://testurl/', + text='SUCCESS', status_code=200) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + resp = s.get('http://testurl/', + endpoint_filter={'service_type': 'compute'}) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.text, 'SUCCESS') + + def test_invalid_auth_response_dict(self): + self.stub_auth(json={'hello': 'world'}) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', + authenticated=True) + + def test_invalid_auth_response_type(self): + self.stub_url('POST', ['tokens'], text='testdata') + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', + authenticated=True) + + def test_invalidate_response(self): + resp_data1 = copy.deepcopy(self.TEST_RESPONSE_DICT) + resp_data2 = copy.deepcopy(self.TEST_RESPONSE_DICT) + + resp_data1['access']['token']['id'] = 'token1' + resp_data2['access']['token']['id'] = 'token2' + + auth_responses = [{'json': resp_data1}, {'json': resp_data2}] + self.stub_auth(response_list=auth_responses) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertEqual('token1', s.get_token()) + self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) + + a.invalidate() + self.assertEqual('token2', s.get_token()) + self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers()) + + def test_doesnt_log_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + password = uuid.uuid4().hex + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=password) + s = session.Session(auth=a) + self.assertEqual(self.TEST_TOKEN, s.get_token()) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + self.assertNotIn(password, self.logger.output) + + def test_password_with_no_user_id_or_name(self): + self.assertRaises(TypeError, + v2.Password, self.TEST_URL, password=self.TEST_PASS) diff --git a/keystoneclient/tests/unit/auth/test_identity_v3.py b/keystoneclient/tests/unit/auth/test_identity_v3.py new file mode 100644 index 0000000..29cbb0e --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_identity_v3.py @@ -0,0 +1,490 @@ +# 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 copy +import uuid + +from keystoneclient import access +from keystoneclient.auth.identity import v3 +from keystoneclient import client +from keystoneclient import exceptions +from keystoneclient import fixture +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class V3IdentityPlugin(utils.TestCase): + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + TEST_PASS = 'password' + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "url": "http://cdn.admin-nets.local:8774/v1.0/", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:8774/v1.0", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "interface": "admin" + }], + "type": "nova_compat" + }, { + "endpoints": [{ + "url": "http://nova/novapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://nova/novapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://nova/novapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "compute", + "name": "nova", + }, { + "endpoints": [{ + "url": "http://glance/glanceapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://glance/glanceapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://glance/glanceapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "internal" + }, { + "url": TEST_ADMIN_URL, + "region": "RegionOne", + "interface": "admin" + }], + "type": "identity" + }, { + "endpoints": [{ + "url": "http://swift/swiftapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://swift/swiftapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://swift/swiftapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "object-store" + }] + + def setUp(self): + super(V3IdentityPlugin, self).setUp() + + V3_URL = "%sv3" % self.TEST_URL + self.TEST_DISCOVERY_RESPONSE = { + 'versions': {'values': [fixture.V3Discovery(V3_URL)]}} + + self.TEST_RESPONSE_DICT = { + "token": { + "methods": [ + "token", + "password" + ], + + "expires_at": "2020-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_TENANT_ID, + "name": self.TEST_TENANT_NAME + }, + "user": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_USER, + "name": self.TEST_USER + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + "catalog": self.TEST_SERVICE_CATALOG + }, + } + self.TEST_PROJECTS_RESPONSE = { + "projects": [ + { + "domain_id": "1789d1", + "enabled": "True", + "id": "263fd9", + "links": { + "self": "https://identity:5000/v3/projects/263fd9" + }, + "name": "Dev Group A" + }, + { + "domain_id": "1789d1", + "enabled": "True", + "id": "e56ad3", + "links": { + "self": "https://identity:5000/v3/projects/e56ad3" + }, + "name": "Dev Group B" + } + ], + "links": { + "self": "https://identity:5000/v3/projects", + } + } + + def stub_auth(self, subject_token=None, **kwargs): + if not subject_token: + subject_token = self.TEST_TOKEN + + self.stub_url('POST', ['auth', 'tokens'], + headers={'X-Subject-Token': subject_token}, **kwargs) + + def test_authenticate_with_username_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}}}} + + self.assertRequestBodyIs(json=req) + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_username_password_unscoped(self): + del self.TEST_RESPONSE_DICT['token']['catalog'] + del self.TEST_RESPONSE_DICT['token']['project'] + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url(method="GET", json=self.TEST_DISCOVERY_RESPONSE) + test_user_id = self.TEST_RESPONSE_DICT['token']['user']['id'] + self.stub_url(method="GET", + json=self.TEST_PROJECTS_RESPONSE, + parts=['users', test_user_id, 'projects']) + + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + cs = client.Client(session=s, auth_url=self.TEST_URL) + + # As a sanity check on the auth_ref, make sure client has the + # proper user id, that it fetches the right project response + self.assertEqual(test_user_id, a.auth_ref.user_id) + t = cs.projects.list(user=a.auth_ref.user_id) + self.assertEqual(2, len(t)) + + def test_authenticate_with_username_password_domain_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, domain_id=self.TEST_DOMAIN_ID) + s = session.Session(a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}}, + 'scope': {'domain': {'id': self.TEST_DOMAIN_ID}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_username_password_project_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, + project_id=self.TEST_DOMAIN_ID) + s = session.Session(a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}}, + 'scope': {'project': {'id': self.TEST_DOMAIN_ID}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + self.assertEqual(s.auth.auth_ref.project_id, self.TEST_DOMAIN_ID) + + def test_authenticate_with_token(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Token(self.TEST_URL, self.TEST_TOKEN) + s = session.Session(auth=a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['token'], + 'token': {'id': self.TEST_TOKEN}}}} + + self.assertRequestBodyIs(json=req) + + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_expired(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + d = copy.deepcopy(self.TEST_RESPONSE_DICT) + d['token']['expires_at'] = '2000-01-01T00:00:10.000123Z' + + a = v3.Password(self.TEST_URL, username='username', + password='password') + a.auth_ref = access.AccessInfo.factory(body=d) + s = session.Session(auth=a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + self.assertEqual(a.auth_ref['expires_at'], + self.TEST_RESPONSE_DICT['token']['expires_at']) + + def test_with_domain_and_project_scoping(self): + a = v3.Password(self.TEST_URL, username='username', + password='password', project_id='project', + domain_id='domain') + + self.assertRaises(exceptions.AuthorizationFailure, + a.get_token, None) + self.assertRaises(exceptions.AuthorizationFailure, + a.get_headers, None) + + def test_with_trust_id(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, trust_id='trust') + s = session.Session(a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}}, + 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_multiple_mechanisms_factory(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + p = v3.PasswordMethod(username=self.TEST_USER, password=self.TEST_PASS) + t = v3.TokenMethod(token='foo') + a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust') + s = session.Session(a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password', 'token'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}, + 'token': {'id': 'foo'}}, + 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_multiple_mechanisms(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + p = v3.PasswordMethod(username=self.TEST_USER, + password=self.TEST_PASS) + t = v3.TokenMethod(token='foo') + a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust') + s = session.Session(auth=a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password', 'token'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}, + 'token': {'id': 'foo'}}, + 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_multiple_scopes(self): + s = session.Session() + + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, password=self.TEST_PASS, + domain_id='x', project_id='x') + self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) + + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, password=self.TEST_PASS, + domain_id='x', trust_id='x') + self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) + + def _do_service_url_test(self, base_url, endpoint_filter): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', ['path'], + base_url=base_url, + text='SUCCESS', status_code=200) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + resp = s.get('/path', endpoint_filter=endpoint_filter) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(self.requests.last_request.url, base_url + '/path') + + def test_service_url(self): + endpoint_filter = {'service_type': 'compute', + 'interface': 'admin', + 'service_name': 'nova'} + self._do_service_url_test('http://nova/novapi/admin', endpoint_filter) + + def test_service_url_defaults_to_public(self): + endpoint_filter = {'service_type': 'compute'} + self._do_service_url_test('http://nova/novapi/public', endpoint_filter) + + def test_endpoint_filter_without_service_type_fails(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.EndpointNotFound, s.get, '/path', + endpoint_filter={'interface': 'admin'}) + + def test_full_url_overrides_endpoint_filter(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [], + base_url='http://testurl/', + text='SUCCESS', status_code=200) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + resp = s.get('http://testurl/', + endpoint_filter={'service_type': 'compute'}) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.text, 'SUCCESS') + + def test_invalid_auth_response_dict(self): + self.stub_auth(json={'hello': 'world'}) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', + authenticated=True) + + def test_invalid_auth_response_type(self): + self.stub_url('POST', ['auth', 'tokens'], text='testdata') + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', + authenticated=True) + + def test_invalidate_response(self): + auth_responses = [{'status_code': 200, 'json': self.TEST_RESPONSE_DICT, + 'headers': {'X-Subject-Token': 'token1'}}, + {'status_code': 200, 'json': self.TEST_RESPONSE_DICT, + 'headers': {'X-Subject-Token': 'token2'}}] + + self.requests.post('%s/auth/tokens' % self.TEST_URL, auth_responses) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertEqual('token1', s.get_token()) + self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) + a.invalidate() + self.assertEqual('token2', s.get_token()) + self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers()) + + def test_doesnt_log_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + password = uuid.uuid4().hex + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=password) + s = session.Session(a) + self.assertEqual(self.TEST_TOKEN, s.get_token()) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + self.assertNotIn(password, self.logger.output) + + def test_sends_nocatalog(self): + del self.TEST_RESPONSE_DICT['token']['catalog'] + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_PASS, + include_catalog=False) + s = session.Session(auth=a) + + s.get_token() + + auth_url = self.TEST_URL + '/auth/tokens' + self.assertEqual(auth_url, a.token_url) + self.assertEqual(auth_url + '?nocatalog', + self.requests.last_request.url) diff --git a/keystoneclient/tests/unit/auth/test_password.py b/keystoneclient/tests/unit/auth/test_password.py new file mode 100644 index 0000000..c5067c0 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_password.py @@ -0,0 +1,63 @@ +# 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.unit.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) + + def test_options(self): + opts = [o.name for o in self.PLUGIN_CLASS.get_options()] + + allowed_opts = ['user-name', + 'user-domain-id', + 'user-domain-name', + 'user-id', + 'password', + + 'domain-id', + 'domain-name', + 'tenant-id', + 'tenant-name', + 'project-id', + 'project-name', + 'project-domain-id', + 'project-domain-name', + 'trust-id', + 'auth-url'] + + self.assertEqual(set(allowed_opts), set(opts)) + self.assertEqual(len(allowed_opts), len(opts)) diff --git a/keystoneclient/tests/unit/auth/test_token.py b/keystoneclient/tests/unit/auth/test_token.py new file mode 100644 index 0000000..928e2b2 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_token.py @@ -0,0 +1,47 @@ +# 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.unit.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) + + def test_options(self): + opts = [o.name for o in self.PLUGIN_CLASS.get_options()] + + allowed_opts = ['token', + 'domain-id', + 'domain-name', + 'tenant-id', + 'tenant-name', + 'project-id', + 'project-name', + 'project-domain-id', + 'project-domain-name', + 'trust-id', + 'auth-url'] + + self.assertEqual(set(allowed_opts), set(opts)) + self.assertEqual(len(allowed_opts), len(opts)) diff --git a/keystoneclient/tests/unit/auth/test_token_endpoint.py b/keystoneclient/tests/unit/auth/test_token_endpoint.py new file mode 100644 index 0000000..4b5f82c --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_token_endpoint.py @@ -0,0 +1,63 @@ +# 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 testtools import matchers + +from keystoneclient.auth import token_endpoint +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class TokenEndpointTest(utils.TestCase): + + TEST_TOKEN = 'aToken' + TEST_URL = 'http://server/prefix' + + def test_basic_case(self): + self.requests.get(self.TEST_URL, text='body') + + a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) + s = session.Session(auth=a) + + data = s.get(self.TEST_URL, authenticated=True) + + self.assertEqual(data.text, 'body') + self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN) + + def test_basic_endpoint_case(self): + self.stub_url('GET', ['p'], text='body') + a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) + s = session.Session(auth=a) + + data = s.get('/p', + authenticated=True, + endpoint_filter={'service': 'identity'}) + + self.assertEqual(self.TEST_URL, a.get_endpoint(s)) + self.assertEqual('body', data.text) + self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN) + + def test_token_endpoint_options(self): + opt_names = [opt.name for opt in token_endpoint.Token.get_options()] + + self.assertThat(opt_names, matchers.HasLength(2)) + + self.assertIn('token', opt_names) + self.assertIn('endpoint', opt_names) + + def test_token_endpoint_user_id(self): + a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) + s = session.Session() + + # we can't know this information about this sort of plugin + self.assertIsNone(a.get_user_id(s)) + self.assertIsNone(a.get_project_id(s)) diff --git a/keystoneclient/tests/unit/auth/utils.py b/keystoneclient/tests/unit/auth/utils.py new file mode 100644 index 0000000..6580c73 --- /dev/null +++ b/keystoneclient/tests/unit/auth/utils.py @@ -0,0 +1,200 @@ +# 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 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.unit import utils + + +class MockPlugin(base.BaseAuthPlugin): + + INT_DESC = 'test int' + FLOAT_DESC = 'test float' + BOOL_DESC = 'test bool' + STR_DESC = 'test str' + STR_DEFAULT = uuid.uuid4().hex + + def __init__(self, **kwargs): + self._data = kwargs + + def __getitem__(self, key): + return self._data[key] + + def get_token(self, *args, **kwargs): + return 'aToken' + + def get_endpoint(self, *args, **kwargs): + return 'http://test' + + @classmethod + def get_options(cls): + return [ + cfg.IntOpt('a-int', default='3', help=cls.INT_DESC), + cfg.BoolOpt('a-bool', help=cls.BOOL_DESC), + cfg.FloatOpt('a-float', help=cls.FLOAT_DESC), + cfg.StrOpt('a-str', help=cls.STR_DESC, default=cls.STR_DEFAULT), + ] + + +class MockManager(object): + + def __init__(self, driver): + self.driver = driver + + +def mock_plugin(f): + @functools.wraps(f) + def inner(*args, **kwargs): + with mock.patch.object(base, 'get_plugin_class') as m: + m.return_value = MockPlugin + args = list(args) + [m] + return f(*args, **kwargs) + + return inner + + +class TestCase(utils.TestCase): + + GROUP = 'auth' + V2PASS = 'v2password' + V3TOKEN = 'v3token' + + a_int = 88 + a_float = 88.8 + a_bool = False + + TEST_VALS = {'a_int': a_int, + 'a_float': a_float, + 'a_bool': a_bool} + + 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/keystoneclient/tests/unit/client_fixtures.py b/keystoneclient/tests/unit/client_fixtures.py new file mode 100644 index 0000000..b226e32 --- /dev/null +++ b/keystoneclient/tests/unit/client_fixtures.py @@ -0,0 +1,597 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 os + +import fixtures +from oslo_serialization import jsonutils +from oslo_utils import timeutils +import six +import testresources + +from keystoneclient.common import cms +from keystoneclient import utils + + +TESTDIR = os.path.dirname(os.path.abspath(__file__)) +ROOTDIR = os.path.normpath(os.path.join(TESTDIR, '..', '..', '..')) +CERTDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'certs') +CMSDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'cms') +KEYDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'private') + + +def _hash_signed_token_safe(signed_text, **kwargs): + if isinstance(signed_text, six.text_type): + signed_text = signed_text.encode('utf-8') + return utils.hash_signed_token(signed_text, **kwargs) + + +class Examples(fixtures.Fixture): + """Example tokens and certs loaded from the examples directory. + + To use this class correctly, the module needs to override the test suite + class to use testresources.OptimisingTestSuite (otherwise the files will + be read on every test). This is done by defining a load_tests function + in the module, like this: + + def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) + + (see http://docs.python.org/2/library/unittest.html#load-tests-protocol ) + + """ + + def setUp(self): + super(Examples, self).setUp() + + # The data for several tests are signed using openssl and are stored in + # files in the signing subdirectory. In order to keep the values + # consistent between the tests and the signed documents, we read them + # in for use in the tests. + with open(os.path.join(CMSDIR, 'auth_token_scoped.json')) as f: + self.TOKEN_SCOPED_DATA = cms.cms_to_token(f.read()) + + with open(os.path.join(CMSDIR, 'auth_token_scoped.pem')) as f: + self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read()) + self.SIGNED_TOKEN_SCOPED_HASH = _hash_signed_token_safe( + self.SIGNED_TOKEN_SCOPED) + self.SIGNED_TOKEN_SCOPED_HASH_SHA256 = _hash_signed_token_safe( + self.SIGNED_TOKEN_SCOPED, mode='sha256') + with open(os.path.join(CMSDIR, 'auth_token_unscoped.pem')) as f: + self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pem')) as f: + self.SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read()) + self.SIGNED_v3_TOKEN_SCOPED_HASH = _hash_signed_token_safe( + self.SIGNED_v3_TOKEN_SCOPED) + self.SIGNED_v3_TOKEN_SCOPED_HASH_SHA256 = _hash_signed_token_safe( + self.SIGNED_v3_TOKEN_SCOPED, mode='sha256') + with open(os.path.join(CMSDIR, 'auth_token_revoked.pem')) as f: + self.REVOKED_TOKEN = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_token_scoped_expired.pem')) as f: + self.SIGNED_TOKEN_SCOPED_EXPIRED = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pem')) as f: + self.REVOKED_v3_TOKEN = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_token_scoped.pkiz')) as f: + self.SIGNED_TOKEN_SCOPED_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_token_unscoped.pkiz')) as f: + self.SIGNED_TOKEN_UNSCOPED_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pkiz')) as f: + self.SIGNED_v3_TOKEN_SCOPED_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_token_revoked.pkiz')) as f: + self.REVOKED_TOKEN_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, + 'auth_token_scoped_expired.pkiz')) as f: + self.SIGNED_TOKEN_SCOPED_EXPIRED_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pkiz')) as f: + self.REVOKED_v3_TOKEN_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'revocation_list.json')) as f: + self.REVOCATION_LIST = jsonutils.loads(f.read()) + with open(os.path.join(CMSDIR, 'revocation_list.pem')) as f: + self.SIGNED_REVOCATION_LIST = jsonutils.dumps({'signed': f.read()}) + + self.SIGNING_CERT_FILE = os.path.join(CERTDIR, 'signing_cert.pem') + with open(self.SIGNING_CERT_FILE) as f: + self.SIGNING_CERT = f.read() + + self.KERBEROS_BIND = 'USER@REALM' + + self.SIGNING_KEY_FILE = os.path.join(KEYDIR, 'signing_key.pem') + with open(self.SIGNING_KEY_FILE) as f: + self.SIGNING_KEY = f.read() + + self.SIGNING_CA_FILE = os.path.join(CERTDIR, 'cacert.pem') + with open(self.SIGNING_CA_FILE) as f: + self.SIGNING_CA = f.read() + + self.UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d" + self.UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df' + self.UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776' + self.UUID_TOKEN_BIND = '3fc54048ad64405c98225ce0897af7c5' + self.UUID_TOKEN_UNKNOWN_BIND = '8885fdf4d42e4fb9879e6379fa1eaf48' + self.VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726' + self.v3_UUID_TOKEN_DEFAULT = '5603457654b346fdbb93437bfe76f2f1' + self.v3_UUID_TOKEN_UNSCOPED = 'd34835fdaec447e695a0a024d84f8d79' + self.v3_UUID_TOKEN_DOMAIN_SCOPED = 'e8a7b63aaa4449f38f0c5c05c3581792' + self.v3_UUID_TOKEN_BIND = '2f61f73e1c854cbb9534c487f9bd63c2' + self.v3_UUID_TOKEN_UNKNOWN_BIND = '7ed9781b62cd4880b8d8c6788ab1d1e2' + + revoked_token = self.REVOKED_TOKEN + if isinstance(revoked_token, six.text_type): + revoked_token = revoked_token.encode('utf-8') + self.REVOKED_TOKEN_HASH = utils.hash_signed_token(revoked_token) + self.REVOKED_TOKEN_HASH_SHA256 = utils.hash_signed_token(revoked_token, + mode='sha256') + self.REVOKED_TOKEN_LIST = ( + {'revoked': [{'id': self.REVOKED_TOKEN_HASH, + 'expires': timeutils.utcnow()}]}) + self.REVOKED_TOKEN_LIST_JSON = jsonutils.dumps(self.REVOKED_TOKEN_LIST) + + revoked_v3_token = self.REVOKED_v3_TOKEN + if isinstance(revoked_v3_token, six.text_type): + revoked_v3_token = revoked_v3_token.encode('utf-8') + self.REVOKED_v3_TOKEN_HASH = utils.hash_signed_token(revoked_v3_token) + hash = utils.hash_signed_token(revoked_v3_token, mode='sha256') + self.REVOKED_v3_TOKEN_HASH_SHA256 = hash + self.REVOKED_v3_TOKEN_LIST = ( + {'revoked': [{'id': self.REVOKED_v3_TOKEN_HASH, + 'expires': timeutils.utcnow()}]}) + self.REVOKED_v3_TOKEN_LIST_JSON = jsonutils.dumps( + self.REVOKED_v3_TOKEN_LIST) + + revoked_token_pkiz = self.REVOKED_TOKEN_PKIZ + if isinstance(revoked_token_pkiz, six.text_type): + revoked_token_pkiz = revoked_token_pkiz.encode('utf-8') + self.REVOKED_TOKEN_PKIZ_HASH = utils.hash_signed_token( + revoked_token_pkiz) + revoked_v3_token_pkiz = self.REVOKED_v3_TOKEN_PKIZ + if isinstance(revoked_v3_token_pkiz, six.text_type): + revoked_v3_token_pkiz = revoked_v3_token_pkiz.encode('utf-8') + self.REVOKED_v3_PKIZ_TOKEN_HASH = utils.hash_signed_token( + revoked_v3_token_pkiz) + + self.REVOKED_TOKEN_PKIZ_LIST = ( + {'revoked': [{'id': self.REVOKED_TOKEN_PKIZ_HASH, + 'expires': timeutils.utcnow()}, + {'id': self.REVOKED_v3_PKIZ_TOKEN_HASH, + 'expires': timeutils.utcnow()}, + ]}) + self.REVOKED_TOKEN_PKIZ_LIST_JSON = jsonutils.dumps( + self.REVOKED_TOKEN_PKIZ_LIST) + + self.SIGNED_TOKEN_SCOPED_KEY = cms.cms_hash_token( + self.SIGNED_TOKEN_SCOPED) + self.SIGNED_TOKEN_UNSCOPED_KEY = cms.cms_hash_token( + self.SIGNED_TOKEN_UNSCOPED) + self.SIGNED_v3_TOKEN_SCOPED_KEY = cms.cms_hash_token( + self.SIGNED_v3_TOKEN_SCOPED) + + self.SIGNED_TOKEN_SCOPED_PKIZ_KEY = cms.cms_hash_token( + self.SIGNED_TOKEN_SCOPED_PKIZ) + self.SIGNED_TOKEN_UNSCOPED_PKIZ_KEY = cms.cms_hash_token( + self.SIGNED_TOKEN_UNSCOPED_PKIZ) + self.SIGNED_v3_TOKEN_SCOPED_PKIZ_KEY = cms.cms_hash_token( + self.SIGNED_v3_TOKEN_SCOPED_PKIZ) + + self.INVALID_SIGNED_TOKEN = ( + "MIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + "0000000000000000000000000000000000000000000000000000000000000000" + "1111111111111111111111111111111111111111111111111111111111111111" + "2222222222222222222222222222222222222222222222222222222222222222" + "3333333333333333333333333333333333333333333333333333333333333333" + "4444444444444444444444444444444444444444444444444444444444444444" + "5555555555555555555555555555555555555555555555555555555555555555" + "6666666666666666666666666666666666666666666666666666666666666666" + "7777777777777777777777777777777777777777777777777777777777777777" + "8888888888888888888888888888888888888888888888888888888888888888" + "9999999999999999999999999999999999999999999999999999999999999999" + "0000000000000000000000000000000000000000000000000000000000000000") + + self.INVALID_SIGNED_PKIZ_TOKEN = ( + "PKIZ_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + "0000000000000000000000000000000000000000000000000000000000000000" + "1111111111111111111111111111111111111111111111111111111111111111" + "2222222222222222222222222222222222222222222222222222222222222222" + "3333333333333333333333333333333333333333333333333333333333333333" + "4444444444444444444444444444444444444444444444444444444444444444" + "5555555555555555555555555555555555555555555555555555555555555555" + "6666666666666666666666666666666666666666666666666666666666666666" + "7777777777777777777777777777777777777777777777777777777777777777" + "8888888888888888888888888888888888888888888888888888888888888888" + "9999999999999999999999999999999999999999999999999999999999999999" + "0000000000000000000000000000000000000000000000000000000000000000") + + # JSON responses keyed by token ID + self.TOKEN_RESPONSES = { + self.UUID_TOKEN_DEFAULT: { + 'access': { + 'token': { + 'id': self.UUID_TOKEN_DEFAULT, + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + self.VALID_DIABLO_TOKEN: { + 'access': { + 'token': { + 'id': self.VALID_DIABLO_TOKEN, + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenantId': 'tenant_id1', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + self.UUID_TOKEN_UNSCOPED: { + 'access': { + 'token': { + 'id': self.UUID_TOKEN_UNSCOPED, + 'expires': '2020-01-01T00:00:10.000123Z', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + self.UUID_TOKEN_NO_SERVICE_CATALOG: { + 'access': { + 'token': { + 'id': 'valid-token', + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + } + }, + }, + self.UUID_TOKEN_BIND: { + 'access': { + 'token': { + 'bind': {'kerberos': self.KERBEROS_BIND}, + 'id': self.UUID_TOKEN_BIND, + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + self.UUID_TOKEN_UNKNOWN_BIND: { + 'access': { + 'token': { + 'bind': {'FOO': 'BAR'}, + 'id': self.UUID_TOKEN_UNKNOWN_BIND, + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + self.v3_UUID_TOKEN_DEFAULT: { + 'token': { + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + self.v3_UUID_TOKEN_UNSCOPED: { + 'token': { + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + } + } + }, + self.v3_UUID_TOKEN_DOMAIN_SCOPED: { + 'token': { + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1', + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + self.SIGNED_TOKEN_SCOPED_KEY: { + 'access': { + 'token': { + 'id': self.SIGNED_TOKEN_SCOPED_KEY, + 'expires': '2020-01-01T00:00:10.000123Z', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'tenantId': 'tenant_id1', + 'tenantName': 'tenant_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + self.SIGNED_TOKEN_UNSCOPED_KEY: { + 'access': { + 'token': { + 'id': self.SIGNED_TOKEN_UNSCOPED_KEY, + 'expires': '2020-01-01T00:00:10.000123Z', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + self.SIGNED_v3_TOKEN_SCOPED_KEY: { + 'token': { + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'} + ], + 'catalog': {} + } + }, + self.v3_UUID_TOKEN_BIND: { + 'token': { + 'bind': {'kerberos': self.KERBEROS_BIND}, + 'methods': ['password'], + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + self.v3_UUID_TOKEN_UNKNOWN_BIND: { + 'token': { + 'bind': {'FOO': 'BAR'}, + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + } + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_PKIZ_KEY] = ( + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY]) + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_PKIZ_KEY] = ( + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_KEY]) + self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_PKIZ_KEY] = ( + self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY]) + + self.JSON_TOKEN_RESPONSES = dict([(k, jsonutils.dumps(v)) for k, v in + six.iteritems(self.TOKEN_RESPONSES)]) + + +EXAMPLES_RESOURCE = testresources.FixtureResource(Examples()) + + +class HackingCode(fixtures.Fixture): + """A fixture to house the various code examples for the keystoneclient + hacking style checks. + """ + + oslo_namespace_imports = { + 'code': """ + import oslo.utils + import oslo_utils + import oslo.utils.encodeutils + import oslo_utils.encodeutils + from oslo import utils + from oslo.utils import encodeutils + from oslo_utils import encodeutils + + import oslo.serialization + import oslo_serialization + import oslo.serialization.jsonutils + import oslo_serialization.jsonutils + from oslo import serialization + from oslo.serialization import jsonutils + from oslo_serialization import jsonutils + + import oslo.config + import oslo_config + import oslo.config.cfg + import oslo_config.cfg + from oslo import config + from oslo.config import cfg + from oslo_config import cfg + + import oslo.i18n + import oslo_i18n + import oslo.i18n.log + import oslo_i18n.log + from oslo import i18n + from oslo.i18n import log + from oslo_i18n import log + """, + 'expected_errors': [ + (1, 0, 'K333'), + (3, 0, 'K333'), + (5, 0, 'K333'), + (6, 0, 'K333'), + (9, 0, 'K333'), + (11, 0, 'K333'), + (13, 0, 'K333'), + (14, 0, 'K333'), + (17, 0, 'K333'), + (19, 0, 'K333'), + (21, 0, 'K333'), + (22, 0, 'K333'), + (25, 0, 'K333'), + (27, 0, 'K333'), + (29, 0, 'K333'), + (30, 0, 'K333'), + ], + } diff --git a/keystoneclient/tests/unit/generic/__init__.py b/keystoneclient/tests/unit/generic/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/keystoneclient/tests/unit/generic/__init__.py diff --git a/keystoneclient/tests/unit/generic/test_client.py b/keystoneclient/tests/unit/generic/test_client.py new file mode 100644 index 0000000..e56e3df --- /dev/null +++ b/keystoneclient/tests/unit/generic/test_client.py @@ -0,0 +1,64 @@ +# Copyright 2014 OpenStack Foundation +# All Rights Reserved. +# +# 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 oslo_serialization import jsonutils + +from keystoneclient.generic import client +from keystoneclient.tests.unit import utils + +BASE_HOST = 'http://keystone.example.com' +BASE_URL = "%s:5000/" % BASE_HOST +V2_URL = "%sv2.0" % BASE_URL + +EXTENSION_NAMESPACE = "http://docs.openstack.org/identity/api/ext/OS-FAKE/v1.0" +EXTENSION_DESCRIBED = {"href": "https://github.com/openstack/identity-api", + "rel": "describedby", + "type": "text/html"} + +EXTENSION_ALIAS_FOO = "OS-FAKE-FOO" +EXTENSION_NAME_FOO = "OpenStack Keystone Fake Extension Foo" +EXTENSION_FOO = {"alias": EXTENSION_ALIAS_FOO, + "description": "Fake Foo extension to V2.0 API.", + "links": [EXTENSION_DESCRIBED], + "name": EXTENSION_NAME_FOO, + "namespace": EXTENSION_NAMESPACE, + "updated": '2014-01-08T00:00:00Z'} + +EXTENSION_ALIAS_BAR = "OS-FAKE-BAR" +EXTENSION_NAME_BAR = "OpenStack Keystone Fake Extension Bar" +EXTENSION_BAR = {"alias": EXTENSION_ALIAS_BAR, + "description": "Fake Bar extension to V2.0 API.", + "links": [EXTENSION_DESCRIBED], + "name": EXTENSION_NAME_BAR, + "namespace": EXTENSION_NAMESPACE, + "updated": '2014-01-08T00:00:00Z'} + + +def _create_extension_list(extensions): + return jsonutils.dumps({'extensions': {'values': extensions}}) + + +EXTENSION_LIST = _create_extension_list([EXTENSION_FOO, EXTENSION_BAR]) + + +class ClientDiscoveryTests(utils.TestCase): + + def test_discover_extensions_v2(self): + self.requests.get("%s/extensions" % V2_URL, text=EXTENSION_LIST) + extensions = client.Client().discover_extensions(url=V2_URL) + self.assertIn(EXTENSION_ALIAS_FOO, extensions) + self.assertEqual(extensions[EXTENSION_ALIAS_FOO], EXTENSION_NAME_FOO) + self.assertIn(EXTENSION_ALIAS_BAR, extensions) + self.assertEqual(extensions[EXTENSION_ALIAS_BAR], EXTENSION_NAME_BAR) diff --git a/keystoneclient/tests/unit/generic/test_shell.py b/keystoneclient/tests/unit/generic/test_shell.py new file mode 100644 index 0000000..ad457aa --- /dev/null +++ b/keystoneclient/tests/unit/generic/test_shell.py @@ -0,0 +1,129 @@ +# Copyright 2014 OpenStack Foundation +# All Rights Reserved. +# +# 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 six import moves + +from keystoneclient.generic import shell +from keystoneclient.tests.unit import utils + + +class DoDiscoverTest(utils.TestCase): + """Unit tests for do_discover function.""" + foo_version = { + 'id': 'foo_id', + 'status': 'foo_status', + 'url': 'http://foo/url', + } + bar_version = { + 'id': 'bar_id', + 'status': 'bar_status', + 'url': 'http://bar/url', + } + foo_extension = { + 'foo': 'foo_extension', + 'message': 'extension_message', + 'bar': 'bar_extension', + } + stub_message = 'This is a stub message' + + def setUp(self): + super(DoDiscoverTest, self).setUp() + + self.client_mock = mock.Mock() + self.client_mock.discover.return_value = {} + + def _execute_discover(self): + """Call do_discover function and capture output + + :returns: captured output is returned + """ + with mock.patch('sys.stdout', + new_callable=moves.StringIO) as mock_stdout: + shell.do_discover(self.client_mock, args=None) + output = mock_stdout.getvalue() + return output + + def _check_version_print(self, output, version): + """Checks all api version's parameters are present in output.""" + self.assertIn(version['id'], output) + self.assertIn(version['status'], output) + self.assertIn(version['url'], output) + + def test_no_keystones(self): + # No servers configured for client, + # corresponding message should be printed + output = self._execute_discover() + self.assertIn('No Keystone-compatible endpoint found', output) + + def test_endpoint(self): + # Endpoint is configured for client, + # client's discover method should be called with that value + self.client_mock.endpoint = 'Some non-empty value' + shell.do_discover(self.client_mock, args=None) + self.client_mock.discover.assert_called_with(self.client_mock.endpoint) + + def test_auth_url(self): + # No endpoint provided for client, but there is an auth_url + # client's discover method should be called with auth_url value + self.client_mock.endpoint = False + self.client_mock.auth_url = 'Some non-empty value' + shell.do_discover(self.client_mock, args=None) + self.client_mock.discover.assert_called_with(self.client_mock.auth_url) + + def test_empty(self): + # No endpoint or auth_url is configured for client. + # client.discover() should be called without parameters + self.client_mock.endpoint = False + self.client_mock.auth_url = False + shell.do_discover(self.client_mock, args=None) + self.client_mock.discover.assert_called_with() + + def test_message(self): + # If client.discover() result contains message - it should be printed + self.client_mock.discover.return_value = {'message': self.stub_message} + output = self._execute_discover() + self.assertIn(self.stub_message, output) + + def test_versions(self): + # Every version in client.discover() result should be printed + # and client.discover_extension() should be called on its url + self.client_mock.discover.return_value = { + 'foo': self.foo_version, + 'bar': self.bar_version, + } + self.client_mock.discover_extensions.return_value = {} + output = self._execute_discover() + self._check_version_print(output, self.foo_version) + self._check_version_print(output, self.bar_version) + + discover_extension_calls = [ + mock.call(self.foo_version['url']), + mock.call(self.bar_version['url']), + ] + + self.client_mock.discover_extensions.assert_has_calls( + discover_extension_calls, + any_order=True) + + def test_extensions(self): + # Every extension's parameters should be printed + # Extension's message should be omitted + self.client_mock.discover.return_value = {'foo': self.foo_version} + self.client_mock.discover_extensions.return_value = self.foo_extension + output = self._execute_discover() + self.assertIn(self.foo_extension['foo'], output) + self.assertIn(self.foo_extension['bar'], output) + self.assertNotIn(self.foo_extension['message'], output) diff --git a/keystoneclient/tests/unit/test_auth_token_middleware.py b/keystoneclient/tests/unit/test_auth_token_middleware.py new file mode 100644 index 0000000..f3b523d --- /dev/null +++ b/keystoneclient/tests/unit/test_auth_token_middleware.py @@ -0,0 +1,1939 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 calendar +import datetime +import json +import os +import shutil +import stat +import tempfile +import time +import uuid + +import fixtures +import iso8601 +import mock +from oslo_serialization import jsonutils +from oslo_utils import timeutils +from requests_mock.contrib import fixture as mock_fixture +import six +from six.moves.urllib import parse as urlparse +import testresources +import testtools +from testtools import matchers +import webob + +from keystoneclient import access +from keystoneclient.common import cms +from keystoneclient import exceptions +from keystoneclient import fixture +from keystoneclient.middleware import auth_token +from keystoneclient.openstack.common import memorycache +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit import utils + + +EXPECTED_V2_DEFAULT_ENV_RESPONSE = { + 'HTTP_X_IDENTITY_STATUS': 'Confirmed', + 'HTTP_X_TENANT_ID': 'tenant_id1', + 'HTTP_X_TENANT_NAME': 'tenant_name1', + 'HTTP_X_USER_ID': 'user_id1', + 'HTTP_X_USER_NAME': 'user_name1', + 'HTTP_X_ROLES': 'role1,role2', + 'HTTP_X_USER': 'user_name1', # deprecated (diablo-compat) + 'HTTP_X_TENANT': 'tenant_name1', # deprecated (diablo-compat) + 'HTTP_X_ROLE': 'role1,role2', # deprecated (diablo-compat) +} + + +BASE_HOST = 'https://keystone.example.com:1234' +BASE_URI = '%s/testadmin' % BASE_HOST +FAKE_ADMIN_TOKEN_ID = 'admin_token2' +FAKE_ADMIN_TOKEN = jsonutils.dumps( + {'access': {'token': {'id': FAKE_ADMIN_TOKEN_ID, + 'expires': '2022-10-03T16:58:01Z'}}}) + + +VERSION_LIST_v2 = jsonutils.dumps(fixture.DiscoveryList(href=BASE_URI, + v3=False)) +VERSION_LIST_v3 = jsonutils.dumps(fixture.DiscoveryList(href=BASE_URI)) + +ERROR_TOKEN = '7ae290c2a06244c4b41692eb4e9225f2' +MEMCACHED_SERVERS = ['localhost:11211'] +MEMCACHED_AVAILABLE = None + + +def memcached_available(): + """Do a sanity check against memcached. + + Returns ``True`` if the following conditions are met (otherwise, returns + ``False``): + + - ``python-memcached`` is installed + - a usable ``memcached`` instance is available via ``MEMCACHED_SERVERS`` + - the client is able to set and get a key/value pair + + """ + global MEMCACHED_AVAILABLE + + if MEMCACHED_AVAILABLE is None: + try: + import memcache + c = memcache.Client(MEMCACHED_SERVERS) + c.set('ping', 'pong', time=1) + MEMCACHED_AVAILABLE = c.get('ping') == 'pong' + except ImportError: + MEMCACHED_AVAILABLE = False + + return MEMCACHED_AVAILABLE + + +def cleanup_revoked_file(filename): + try: + os.remove(filename) + except OSError: + pass + + +class TimezoneFixture(fixtures.Fixture): + @staticmethod + def supported(): + # tzset is only supported on Unix. + return hasattr(time, 'tzset') + + def __init__(self, new_tz): + super(TimezoneFixture, self).__init__() + self.tz = new_tz + self.old_tz = os.environ.get('TZ') + + def setUp(self): + super(TimezoneFixture, self).setUp() + if not self.supported(): + raise NotImplementedError('timezone override is not supported.') + os.environ['TZ'] = self.tz + time.tzset() + self.addCleanup(self.cleanup) + + def cleanup(self): + if self.old_tz is not None: + os.environ['TZ'] = self.old_tz + elif 'TZ' in os.environ: + del os.environ['TZ'] + time.tzset() + + +class TimeFixture(fixtures.Fixture): + + def __init__(self, new_time, normalize=True): + super(TimeFixture, self).__init__() + if isinstance(new_time, six.string_types): + new_time = timeutils.parse_isotime(new_time) + if normalize: + new_time = timeutils.normalize_time(new_time) + self.new_time = new_time + + def setUp(self): + super(TimeFixture, self).setUp() + timeutils.set_time_override(self.new_time) + self.addCleanup(timeutils.clear_time_override) + + +class FakeApp(object): + """This represents a WSGI app protected by the auth_token middleware.""" + + SUCCESS = b'SUCCESS' + + def __init__(self, expected_env=None): + self.expected_env = dict(EXPECTED_V2_DEFAULT_ENV_RESPONSE) + + if expected_env: + self.expected_env.update(expected_env) + + def __call__(self, env, start_response): + for k, v in self.expected_env.items(): + assert env[k] == v, '%s != %s' % (env[k], v) + + resp = webob.Response() + resp.body = FakeApp.SUCCESS + return resp(env, start_response) + + +class v3FakeApp(FakeApp): + """This represents a v3 WSGI app protected by the auth_token middleware.""" + + def __init__(self, expected_env=None): + + # with v3 additions, these are for the DEFAULT TOKEN + v3_default_env_additions = { + 'HTTP_X_PROJECT_ID': 'tenant_id1', + 'HTTP_X_PROJECT_NAME': 'tenant_name1', + 'HTTP_X_PROJECT_DOMAIN_ID': 'domain_id1', + 'HTTP_X_PROJECT_DOMAIN_NAME': 'domain_name1', + 'HTTP_X_USER_DOMAIN_ID': 'domain_id1', + 'HTTP_X_USER_DOMAIN_NAME': 'domain_name1' + } + + if expected_env: + v3_default_env_additions.update(expected_env) + + super(v3FakeApp, self).__init__(v3_default_env_additions) + + +class BaseAuthTokenMiddlewareTest(testtools.TestCase): + """Base test class for auth_token middleware. + + All the tests allow for running with auth_token + configured for receiving v2 or v3 tokens, with the + choice being made by passing configuration data into + setUp(). + + The base class will, by default, run all the tests + expecting v2 token formats. Child classes can override + this to specify, for instance, v3 format. + + """ + def setUp(self, expected_env=None, auth_version=None, fake_app=None): + testtools.TestCase.setUp(self) + + self.expected_env = expected_env or dict() + self.fake_app = fake_app or FakeApp + self.middleware = None + + self.conf = { + 'identity_uri': 'https://keystone.example.com:1234/testadmin/', + 'signing_dir': client_fixtures.CERTDIR, + 'auth_version': auth_version, + 'auth_uri': 'https://keystone.example.com:1234', + } + + self.auth_version = auth_version + self.response_status = None + self.response_headers = None + + self.requests = self.useFixture(mock_fixture.Fixture()) + + def set_middleware(self, expected_env=None, conf=None): + """Configure the class ready to call the auth_token middleware. + + Set up the various fake items needed to run the middleware. + Individual tests that need to further refine these can call this + function to override the class defaults. + + """ + if conf: + self.conf.update(conf) + + if expected_env: + self.expected_env.update(expected_env) + + self.middleware = auth_token.AuthProtocol( + self.fake_app(self.expected_env), self.conf) + self.middleware._iso8601 = iso8601 + + with tempfile.NamedTemporaryFile(dir=self.middleware.signing_dirname, + delete=False) as f: + pass + self.middleware.revoked_file_name = f.name + + self.addCleanup(cleanup_revoked_file, + self.middleware.revoked_file_name) + + self.middleware.token_revocation_list = jsonutils.dumps( + {"revoked": [], "extra": "success"}) + + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + + def assertLastPath(self, path): + if path: + parts = urlparse.urlparse(self.requests.last_request.url) + self.assertEqual(path, parts.path) + else: + self.assertIsNone(self.requests.last_request) + + +class MultiStepAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_fetch_revocation_list_with_expire(self): + self.set_middleware() + + # Get a token, then try to retrieve revocation list and get a 401. + # Get a new token, try to retrieve revocation list and return 200. + self.requests.post("%s/v2.0/tokens" % BASE_URI, text=FAKE_ADMIN_TOKEN) + + text = self.examples.SIGNED_REVOCATION_LIST + self.requests.get("%s/v2.0/tokens/revoked" % BASE_URI, + response_list=[{'status_code': 401}, {'text': text}]) + + fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) + self.assertEqual(fetched_list, self.examples.REVOCATION_LIST) + + # Check that 4 requests have been made + self.assertEqual(len(self.requests.request_history), 4) + + +class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + """Auth Token middleware should understand Diablo keystone responses.""" + def setUp(self): + # pre-diablo only had Tenant ID, which was also the Name + expected_env = { + 'HTTP_X_TENANT_ID': 'tenant_id1', + 'HTTP_X_TENANT_NAME': 'tenant_id1', + # now deprecated (diablo-compat) + 'HTTP_X_TENANT': 'tenant_id1', + } + + super(DiabloAuthTokenMiddlewareTest, self).setUp( + expected_env=expected_env) + + self.requests.get("%s/" % BASE_URI, + text=VERSION_LIST_v2, + status_code=300) + + self.requests.post("%s/v2.0/tokens" % BASE_URI, text=FAKE_ADMIN_TOKEN) + + self.token_id = self.examples.VALID_DIABLO_TOKEN + token_response = self.examples.JSON_TOKEN_RESPONSES[self.token_id] + + url = '%s/v2.0/tokens/%s' % (BASE_URI, self.token_id) + self.requests.get(url, text=token_response) + + self.set_middleware() + + def test_valid_diablo_response(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.token_id + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertIn('keystone.token_info', req.environ) + + +class NoMemcacheAuthToken(BaseAuthTokenMiddlewareTest): + """These tests will not have the memcache module available.""" + + def setUp(self): + super(NoMemcacheAuthToken, self).setUp() + self.useFixture(utils.DisableModuleFixture('memcache')) + + def test_nomemcache(self): + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcached_servers': MEMCACHED_SERVERS, + 'auth_uri': 'https://keystone.example.com:1234', + } + + auth_token.AuthProtocol(FakeApp(), conf) + + +class CachePoolTest(BaseAuthTokenMiddlewareTest): + def test_use_cache_from_env(self): + """If `swift.cache` is set in the environment and `cache` is set in the + config then the env cache is used. + """ + env = {'swift.cache': 'CACHE_TEST'} + conf = { + 'cache': 'swift.cache' + } + self.set_middleware(conf=conf) + self.middleware._token_cache.initialize(env) + with self.middleware._token_cache._cache_pool.reserve() as cache: + self.assertEqual(cache, 'CACHE_TEST') + + def test_not_use_cache_from_env(self): + """If `swift.cache` is set in the environment but `cache` isn't set in + the config then the env cache isn't used. + """ + self.set_middleware() + env = {'swift.cache': 'CACHE_TEST'} + self.middleware._token_cache.initialize(env) + with self.middleware._token_cache._cache_pool.reserve() as cache: + self.assertNotEqual(cache, 'CACHE_TEST') + + def test_multiple_context_managers_share_single_client(self): + self.set_middleware() + token_cache = self.middleware._token_cache + env = {} + token_cache.initialize(env) + + caches = [] + + with token_cache._cache_pool.reserve() as cache: + caches.append(cache) + + with token_cache._cache_pool.reserve() as cache: + caches.append(cache) + + self.assertIs(caches[0], caches[1]) + self.assertEqual(set(caches), set(token_cache._cache_pool)) + + def test_nested_context_managers_create_multiple_clients(self): + self.set_middleware() + env = {} + self.middleware._token_cache.initialize(env) + token_cache = self.middleware._token_cache + + with token_cache._cache_pool.reserve() as outer_cache: + with token_cache._cache_pool.reserve() as inner_cache: + self.assertNotEqual(outer_cache, inner_cache) + + self.assertEqual( + set([inner_cache, outer_cache]), + set(token_cache._cache_pool)) + + +class GeneralAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + testresources.ResourcedTestCase): + """These tests are not affected by the token format + (see CommonAuthTokenMiddlewareTest). + """ + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_will_expire_soon(self): + tenseconds = datetime.datetime.utcnow() + datetime.timedelta( + seconds=10) + self.assertTrue(auth_token.will_expire_soon(tenseconds)) + fortyseconds = datetime.datetime.utcnow() + datetime.timedelta( + seconds=40) + self.assertFalse(auth_token.will_expire_soon(fortyseconds)) + + def test_token_is_v2_accepts_v2(self): + token = self.examples.UUID_TOKEN_DEFAULT + token_response = self.examples.TOKEN_RESPONSES[token] + self.assertTrue(auth_token._token_is_v2(token_response)) + + def test_token_is_v2_rejects_v3(self): + token = self.examples.v3_UUID_TOKEN_DEFAULT + token_response = self.examples.TOKEN_RESPONSES[token] + self.assertFalse(auth_token._token_is_v2(token_response)) + + def test_token_is_v3_rejects_v2(self): + token = self.examples.UUID_TOKEN_DEFAULT + token_response = self.examples.TOKEN_RESPONSES[token] + self.assertFalse(auth_token._token_is_v3(token_response)) + + def test_token_is_v3_accepts_v3(self): + token = self.examples.v3_UUID_TOKEN_DEFAULT + token_response = self.examples.TOKEN_RESPONSES[token] + self.assertTrue(auth_token._token_is_v3(token_response)) + + @testtools.skipUnless(memcached_available(), 'memcached not available') + def test_encrypt_cache_data(self): + conf = { + 'memcached_servers': MEMCACHED_SERVERS, + 'memcache_security_strategy': 'encrypt', + 'memcache_secret_key': 'mysecret' + } + self.set_middleware(conf=conf) + token = b'my_token' + some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4) + expires = timeutils.strtime(some_time_later) + data = ('this_data', expires) + token_cache = self.middleware._token_cache + token_cache.initialize({}) + token_cache._cache_store(token, data) + self.assertEqual(token_cache._cache_get(token), data[0]) + + @testtools.skipUnless(memcached_available(), 'memcached not available') + def test_sign_cache_data(self): + conf = { + 'memcached_servers': MEMCACHED_SERVERS, + 'memcache_security_strategy': 'mac', + 'memcache_secret_key': 'mysecret' + } + self.set_middleware(conf=conf) + token = b'my_token' + some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4) + expires = timeutils.strtime(some_time_later) + data = ('this_data', expires) + token_cache = self.middleware._token_cache + token_cache.initialize({}) + token_cache._cache_store(token, data) + self.assertEqual(token_cache._cache_get(token), data[0]) + + @testtools.skipUnless(memcached_available(), 'memcached not available') + def test_no_memcache_protection(self): + conf = { + 'memcached_servers': MEMCACHED_SERVERS, + 'memcache_secret_key': 'mysecret' + } + self.set_middleware(conf=conf) + token = 'my_token' + some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4) + expires = timeutils.strtime(some_time_later) + data = ('this_data', expires) + token_cache = self.middleware._token_cache + token_cache.initialize({}) + token_cache._cache_store(token, data) + self.assertEqual(token_cache._cache_get(token), data[0]) + + def test_assert_valid_memcache_protection_config(self): + # test missing memcache_secret_key + conf = { + 'memcached_servers': MEMCACHED_SERVERS, + 'memcache_security_strategy': 'Encrypt' + } + self.assertRaises(auth_token.ConfigurationError, self.set_middleware, + conf=conf) + # test invalue memcache_security_strategy + conf = { + 'memcached_servers': MEMCACHED_SERVERS, + 'memcache_security_strategy': 'whatever' + } + self.assertRaises(auth_token.ConfigurationError, self.set_middleware, + conf=conf) + # test missing memcache_secret_key + conf = { + 'memcached_servers': MEMCACHED_SERVERS, + 'memcache_security_strategy': 'mac' + } + self.assertRaises(auth_token.ConfigurationError, self.set_middleware, + conf=conf) + conf = { + 'memcached_servers': MEMCACHED_SERVERS, + 'memcache_security_strategy': 'Encrypt', + 'memcache_secret_key': '' + } + self.assertRaises(auth_token.ConfigurationError, self.set_middleware, + conf=conf) + conf = { + 'memcached_servers': MEMCACHED_SERVERS, + 'memcache_security_strategy': 'mAc', + 'memcache_secret_key': '' + } + self.assertRaises(auth_token.ConfigurationError, self.set_middleware, + conf=conf) + + def test_config_revocation_cache_timeout(self): + conf = { + 'revocation_cache_time': 24, + 'auth_uri': 'https://keystone.example.com:1234', + } + middleware = auth_token.AuthProtocol(self.fake_app, conf) + self.assertEqual(middleware.token_revocation_list_cache_timeout, + datetime.timedelta(seconds=24)) + + def test_conf_values_type_convert(self): + conf = { + 'revocation_cache_time': '24', + 'identity_uri': 'https://keystone.example.com:1234', + 'include_service_catalog': '0', + 'nonexsit_option': '0', + } + + middleware = auth_token.AuthProtocol(self.fake_app, conf) + self.assertEqual(datetime.timedelta(seconds=24), + middleware.token_revocation_list_cache_timeout) + self.assertEqual(False, middleware.include_service_catalog) + self.assertEqual('https://keystone.example.com:1234', + middleware.identity_uri) + self.assertEqual('0', middleware.conf['nonexsit_option']) + + def test_conf_values_type_convert_with_wrong_value(self): + conf = { + 'include_service_catalog': '123', + } + self.assertRaises(auth_token.ConfigurationError, + auth_token.AuthProtocol, self.fake_app, conf) + + +class CommonAuthTokenMiddlewareTest(object): + """These tests are run once using v2 tokens and again using v3 tokens.""" + + def test_init_does_not_call_http(self): + conf = { + 'revocation_cache_time': 1 + } + self.set_middleware(conf=conf) + self.assertLastPath(None) + + def test_init_by_ipv6Addr_auth_host(self): + del self.conf['identity_uri'] + conf = { + 'auth_host': '2001:2013:1:f101::1', + 'auth_port': 1234, + 'auth_protocol': 'http', + 'auth_uri': None, + } + self.set_middleware(conf=conf) + expected_auth_uri = 'http://[2001:2013:1:f101::1]:1234' + self.assertEqual(expected_auth_uri, self.middleware.auth_uri) + + def assert_valid_request_200(self, token, with_catalog=True): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + if with_catalog: + self.assertTrue(req.headers.get('X-Service-Catalog')) + else: + self.assertNotIn('X-Service-Catalog', req.headers) + self.assertEqual(body, [FakeApp.SUCCESS]) + self.assertIn('keystone.token_info', req.environ) + return req + + def test_valid_uuid_request(self): + for _ in range(2): # Do it twice because first result was cached. + token = self.token_dict['uuid_token_default'] + self.assert_valid_request_200(token) + self.assert_valid_last_url(token) + + def test_valid_uuid_request_with_auth_fragments(self): + del self.conf['identity_uri'] + self.conf['auth_protocol'] = 'https' + self.conf['auth_host'] = 'keystone.example.com' + self.conf['auth_port'] = 1234 + self.conf['auth_admin_prefix'] = '/testadmin' + self.set_middleware() + self.assert_valid_request_200(self.token_dict['uuid_token_default']) + self.assert_valid_last_url(self.token_dict['uuid_token_default']) + + def _test_cache_revoked(self, token, revoked_form=None): + # When the token is cached and revoked, 401 is returned. + self.middleware.check_revocations_for_cached = True + + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + + # Token should be cached as ok after this. + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(200, self.response_status) + + # Put it in revocation list. + self.middleware.token_revocation_list = self.get_revocation_list_json( + token_ids=[revoked_form or token]) + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(401, self.response_status) + + def test_cached_revoked_uuid(self): + # When the UUID token is cached and revoked, 401 is returned. + self._test_cache_revoked(self.token_dict['uuid_token_default']) + + def test_valid_signed_request(self): + for _ in range(2): # Do it twice because first result was cached. + self.assert_valid_request_200( + self.token_dict['signed_token_scoped']) + # ensure that signed requests do not generate HTTP traffic + self.assertLastPath(None) + + def test_valid_signed_compressed_request(self): + self.assert_valid_request_200( + self.token_dict['signed_token_scoped_pkiz']) + # ensure that signed requests do not generate HTTP traffic + self.assertLastPath(None) + + def test_revoked_token_receives_401(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.token_dict['revoked_token'] + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + + def test_revoked_token_receives_401_sha256(self): + self.conf['hash_algorithms'] = ['sha256', 'md5'] + self.set_middleware() + self.middleware.token_revocation_list = ( + self.get_revocation_list_json(mode='sha256')) + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.token_dict['revoked_token'] + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + + def test_cached_revoked_pki(self): + # When the PKI token is cached and revoked, 401 is returned. + token = self.token_dict['signed_token_scoped'] + revoked_form = cms.cms_hash_token(token) + self._test_cache_revoked(token, revoked_form) + + def test_cached_revoked_pkiz(self): + # When the PKI token is cached and revoked, 401 is returned. + token = self.token_dict['signed_token_scoped_pkiz'] + revoked_form = cms.cms_hash_token(token) + self._test_cache_revoked(token, revoked_form) + + def test_revoked_token_receives_401_md5_secondary(self): + # When hash_algorithms has 'md5' as the secondary hash and the + # revocation list contains the md5 hash for a token, that token is + # considered revoked so returns 401. + self.conf['hash_algorithms'] = ['sha256', 'md5'] + self.set_middleware() + self.middleware.token_revocation_list = self.get_revocation_list_json() + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.token_dict['revoked_token'] + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + + def _test_revoked_hashed_token(self, token_key): + # If hash_algorithms is set as ['sha256', 'md5'], + # and check_revocations_for_cached is True, + # and a token is in the cache because it was successfully validated + # using the md5 hash, then + # if the token is in the revocation list by md5 hash, it'll be + # rejected and auth_token returns 401. + self.conf['hash_algorithms'] = ['sha256', 'md5'] + self.conf['check_revocations_for_cached'] = True + self.set_middleware() + + token = self.token_dict[token_key] + + # Put the token in the revocation list. + token_hashed = cms.cms_hash_token(token) + self.middleware.token_revocation_list = self.get_revocation_list_json( + token_ids=[token_hashed]) + + # request is using the hashed token, is valid so goes in + # cache using the given hash. + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token_hashed + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(200, self.response_status) + + # This time use the PKI(Z) token + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + + # Should find the token in the cache and revocation list. + self.assertEqual(401, self.response_status) + + def test_revoked_hashed_pki_token(self): + self._test_revoked_hashed_token('signed_token_scoped') + + def test_revoked_hashed_pkiz_token(self): + self._test_revoked_hashed_token('signed_token_scoped_pkiz') + + def get_revocation_list_json(self, token_ids=None, mode=None): + if token_ids is None: + key = 'revoked_token_hash' + (('_' + mode) if mode else '') + token_ids = [self.token_dict[key]] + revocation_list = {'revoked': [{'id': x, 'expires': timeutils.utcnow()} + for x in token_ids]} + return jsonutils.dumps(revocation_list) + + def test_is_signed_token_revoked_returns_false(self): + # explicitly setting an empty revocation list here to document intent + self.middleware.token_revocation_list = jsonutils.dumps( + {"revoked": [], "extra": "success"}) + result = self.middleware.is_signed_token_revoked( + [self.token_dict['revoked_token_hash']]) + self.assertFalse(result) + + def test_is_signed_token_revoked_returns_true(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + result = self.middleware.is_signed_token_revoked( + [self.token_dict['revoked_token_hash']]) + self.assertTrue(result) + + def test_is_signed_token_revoked_returns_true_sha256(self): + self.conf['hash_algorithms'] = ['sha256', 'md5'] + self.set_middleware() + self.middleware.token_revocation_list = ( + self.get_revocation_list_json(mode='sha256')) + result = self.middleware.is_signed_token_revoked( + [self.token_dict['revoked_token_hash_sha256']]) + self.assertTrue(result) + + def test_verify_signed_token_raises_exception_for_revoked_token(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + self.assertRaises(auth_token.InvalidUserToken, + self.middleware.verify_signed_token, + self.token_dict['revoked_token'], + [self.token_dict['revoked_token_hash']]) + + def test_verify_signed_token_raises_exception_for_revoked_token_s256(self): + self.conf['hash_algorithms'] = ['sha256', 'md5'] + self.set_middleware() + self.middleware.token_revocation_list = ( + self.get_revocation_list_json(mode='sha256')) + self.assertRaises(auth_token.InvalidUserToken, + self.middleware.verify_signed_token, + self.token_dict['revoked_token'], + [self.token_dict['revoked_token_hash_sha256'], + self.token_dict['revoked_token_hash']]) + + def test_verify_signed_token_raises_exception_for_revoked_pkiz_token(self): + self.middleware.token_revocation_list = ( + self.examples.REVOKED_TOKEN_PKIZ_LIST_JSON) + self.assertRaises(auth_token.InvalidUserToken, + self.middleware.verify_pkiz_token, + self.token_dict['revoked_token_pkiz'], + [self.token_dict['revoked_token_pkiz_hash']]) + + def assertIsValidJSON(self, text): + json.loads(text) + + def test_verify_signed_token_succeeds_for_unrevoked_token(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + text = self.middleware.verify_signed_token( + self.token_dict['signed_token_scoped'], + [self.token_dict['signed_token_scoped_hash']]) + self.assertIsValidJSON(text) + + def test_verify_signed_compressed_token_succeeds_for_unrevoked_token(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + text = self.middleware.verify_pkiz_token( + self.token_dict['signed_token_scoped_pkiz'], + [self.token_dict['signed_token_scoped_hash']]) + self.assertIsValidJSON(text) + + def test_verify_signed_token_succeeds_for_unrevoked_token_sha256(self): + self.conf['hash_algorithms'] = ['sha256', 'md5'] + self.set_middleware() + self.middleware.token_revocation_list = ( + self.get_revocation_list_json(mode='sha256')) + text = self.middleware.verify_signed_token( + self.token_dict['signed_token_scoped'], + [self.token_dict['signed_token_scoped_hash_sha256'], + self.token_dict['signed_token_scoped_hash']]) + self.assertIsValidJSON(text) + + def test_verify_signing_dir_create_while_missing(self): + tmp_name = uuid.uuid4().hex + test_parent_signing_dir = "/tmp/%s" % tmp_name + self.middleware.signing_dirname = "/tmp/%s/%s" % ((tmp_name,) * 2) + self.middleware.signing_cert_file_name = ( + "%s/test.pem" % self.middleware.signing_dirname) + self.middleware.verify_signing_dir() + # NOTE(wu_wenxiang): Verify if the signing dir was created as expected. + self.assertTrue(os.path.isdir(self.middleware.signing_dirname)) + self.assertTrue(os.access(self.middleware.signing_dirname, os.W_OK)) + self.assertEqual(os.stat(self.middleware.signing_dirname).st_uid, + os.getuid()) + self.assertEqual( + stat.S_IMODE(os.stat(self.middleware.signing_dirname).st_mode), + stat.S_IRWXU) + shutil.rmtree(test_parent_signing_dir) + + def test_get_token_revocation_list_fetched_time_returns_min(self): + self.middleware.token_revocation_list_fetched_time = None + self.middleware.revoked_file_name = '' + self.assertEqual(self.middleware.token_revocation_list_fetched_time, + datetime.datetime.min) + + def test_get_token_revocation_list_fetched_time_returns_mtime(self): + self.middleware.token_revocation_list_fetched_time = None + mtime = os.path.getmtime(self.middleware.revoked_file_name) + fetched_time = datetime.datetime.utcfromtimestamp(mtime) + self.assertEqual(fetched_time, + self.middleware.token_revocation_list_fetched_time) + + @testtools.skipUnless(TimezoneFixture.supported(), + 'TimezoneFixture not supported') + def test_get_token_revocation_list_fetched_time_returns_utc(self): + with TimezoneFixture('UTC-1'): + self.middleware.token_revocation_list = jsonutils.dumps( + self.examples.REVOCATION_LIST) + self.middleware.token_revocation_list_fetched_time = None + fetched_time = self.middleware.token_revocation_list_fetched_time + self.assertTrue(timeutils.is_soon(fetched_time, 1)) + + def test_get_token_revocation_list_fetched_time_returns_value(self): + expected = self.middleware._token_revocation_list_fetched_time + self.assertEqual(self.middleware.token_revocation_list_fetched_time, + expected) + + def test_get_revocation_list_returns_fetched_list(self): + # auth_token uses v2 to fetch this, so don't allow the v3 + # tests to override the fake http connection + self.middleware.token_revocation_list_fetched_time = None + os.remove(self.middleware.revoked_file_name) + self.assertEqual(self.middleware.token_revocation_list, + self.examples.REVOCATION_LIST) + + def test_get_revocation_list_returns_current_list_from_memory(self): + self.assertEqual(self.middleware.token_revocation_list, + self.middleware._token_revocation_list) + + def test_get_revocation_list_returns_current_list_from_disk(self): + in_memory_list = self.middleware.token_revocation_list + self.middleware._token_revocation_list = None + self.assertEqual(self.middleware.token_revocation_list, in_memory_list) + + def test_invalid_revocation_list_raises_service_error(self): + self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI, text='{}') + + self.assertRaises(auth_token.ServiceError, + self.middleware.fetch_revocation_list) + + def test_fetch_revocation_list(self): + # auth_token uses v2 to fetch this, so don't allow the v3 + # tests to override the fake http connection + fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) + self.assertEqual(fetched_list, self.examples.REVOCATION_LIST) + + def test_request_invalid_uuid_token(self): + # remember because we are testing the middleware we stub the connection + # to the keystone server, but this is not what gets returned + invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI + self.requests.get(invalid_uri, text="", status_code=404) + + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = 'invalid-token' + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def test_request_invalid_signed_token(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.examples.INVALID_SIGNED_TOKEN + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(401, self.response_status) + self.assertEqual("Keystone uri='https://keystone.example.com:1234'", + self.response_headers['WWW-Authenticate']) + + def test_request_invalid_signed_pkiz_token(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.examples.INVALID_SIGNED_PKIZ_TOKEN + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(401, self.response_status) + self.assertEqual("Keystone uri='https://keystone.example.com:1234'", + self.response_headers['WWW-Authenticate']) + + def test_request_no_token(self): + req = webob.Request.blank('/') + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def test_request_no_token_log_message(self): + class FakeLog(object): + def __init__(self): + self.msg = None + self.debugmsg = None + + def warn(self, msg=None, *args, **kwargs): + self.msg = msg + + def debug(self, msg=None, *args, **kwargs): + self.debugmsg = msg + + self.middleware.LOG = FakeLog() + self.middleware.delay_auth_decision = False + self.assertRaises(auth_token.InvalidUserToken, + self.middleware._get_user_token_from_header, {}) + self.assertIsNotNone(self.middleware.LOG.msg) + self.assertIsNotNone(self.middleware.LOG.debugmsg) + + def test_request_no_token_http(self): + req = webob.Request.blank('/', environ={'REQUEST_METHOD': 'HEAD'}) + self.set_middleware() + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + self.assertEqual(body, ['']) + + def test_request_blank_token(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = '' + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def _get_cached_token(self, token, mode='md5'): + token_id = cms.cms_hash_token(token, mode=mode) + return self.middleware._token_cache._cache_get(token_id) + + def test_memcache(self): + req = webob.Request.blank('/') + token = self.token_dict['signed_token_scoped'] + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertIsNotNone(self._get_cached_token(token)) + + def test_expired(self): + req = webob.Request.blank('/') + token = self.token_dict['signed_token_scoped_expired'] + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + + def test_memcache_set_invalid_uuid(self): + invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI + self.requests.get(invalid_uri, status_code=404) + + req = webob.Request.blank('/') + token = 'invalid-token' + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertRaises(auth_token.InvalidUserToken, + self._get_cached_token, token) + + def _test_memcache_set_invalid_signed(self, hash_algorithms=None, + exp_mode='md5'): + req = webob.Request.blank('/') + token = self.token_dict['signed_token_scoped_expired'] + req.headers['X-Auth-Token'] = token + if hash_algorithms: + self.conf['hash_algorithms'] = hash_algorithms + self.set_middleware() + self.middleware(req.environ, self.start_fake_response) + self.assertRaises(auth_token.InvalidUserToken, + self._get_cached_token, token, mode=exp_mode) + + def test_memcache_set_invalid_signed(self): + self._test_memcache_set_invalid_signed() + + def test_memcache_set_invalid_signed_sha256_md5(self): + hash_algorithms = ['sha256', 'md5'] + self._test_memcache_set_invalid_signed(hash_algorithms=hash_algorithms, + exp_mode='sha256') + + def test_memcache_set_invalid_signed_sha256(self): + hash_algorithms = ['sha256'] + self._test_memcache_set_invalid_signed(hash_algorithms=hash_algorithms, + exp_mode='sha256') + + def test_memcache_set_expired(self, extra_conf={}, extra_environ={}): + token_cache_time = 10 + conf = { + 'token_cache_time': token_cache_time, + 'signing_dir': client_fixtures.CERTDIR, + } + conf.update(extra_conf) + self.set_middleware(conf=conf) + req = webob.Request.blank('/') + token = self.token_dict['signed_token_scoped'] + req.headers['X-Auth-Token'] = token + req.environ.update(extra_environ) + + now = datetime.datetime.utcnow() + self.useFixture(TimeFixture(now)) + + self.middleware(req.environ, self.start_fake_response) + self.assertIsNotNone(self._get_cached_token(token)) + + timeutils.advance_time_seconds(token_cache_time) + self.assertIsNone(self._get_cached_token(token)) + + def test_swift_memcache_set_expired(self): + extra_conf = {'cache': 'swift.cache'} + extra_environ = {'swift.cache': memorycache.Client()} + self.test_memcache_set_expired(extra_conf, extra_environ) + + def test_http_error_not_cached_token(self): + """Test to don't cache token as invalid on network errors. + + We use UUID tokens since they are the easiest one to reach + get_http_connection. + """ + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = ERROR_TOKEN + self.middleware.http_request_max_retries = 0 + self.middleware(req.environ, self.start_fake_response) + self.assertIsNone(self._get_cached_token(ERROR_TOKEN)) + self.assert_valid_last_url(ERROR_TOKEN) + + def test_http_request_max_retries(self): + times_retry = 10 + + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = ERROR_TOKEN + + conf = {'http_request_max_retries': times_retry} + self.set_middleware(conf=conf) + + with mock.patch('time.sleep') as mock_obj: + self.middleware(req.environ, self.start_fake_response) + + self.assertEqual(mock_obj.call_count, times_retry) + + def test_nocatalog(self): + conf = { + 'include_service_catalog': False + } + self.set_middleware(conf=conf) + self.assert_valid_request_200(self.token_dict['uuid_token_default'], + with_catalog=False) + + def assert_kerberos_bind(self, token, bind_level, + use_kerberos=True, success=True): + conf = { + 'enforce_token_bind': bind_level, + 'auth_version': self.auth_version, + } + self.set_middleware(conf=conf) + + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + + if use_kerberos: + if use_kerberos is True: + req.environ['REMOTE_USER'] = self.examples.KERBEROS_BIND + else: + req.environ['REMOTE_USER'] = use_kerberos + + req.environ['AUTH_TYPE'] = 'Negotiate' + + body = self.middleware(req.environ, self.start_fake_response) + + if success: + self.assertEqual(self.response_status, 200) + self.assertEqual(body, [FakeApp.SUCCESS]) + self.assertIn('keystone.token_info', req.environ) + self.assert_valid_last_url(token) + else: + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'" + ) + + def test_uuid_bind_token_disabled_with_kerb_user(self): + for use_kerberos in [True, False]: + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='disabled', + use_kerberos=use_kerberos, + success=True) + + def test_uuid_bind_token_disabled_with_incorrect_ticket(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos='ronald@MCDONALDS.COM', + success=False) + + def test_uuid_bind_token_permissive_with_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='permissive', + use_kerberos=True, + success=True) + + def test_uuid_bind_token_permissive_without_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='permissive', + use_kerberos=False, + success=False) + + def test_uuid_bind_token_permissive_with_unknown_bind(self): + token = self.token_dict['uuid_token_unknown_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='permissive', + use_kerberos=use_kerberos, + success=True) + + def test_uuid_bind_token_permissive_with_incorrect_ticket(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos='ronald@MCDONALDS.COM', + success=False) + + def test_uuid_bind_token_strict_with_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='strict', + use_kerberos=True, + success=True) + + def test_uuid_bind_token_strict_with_kerbout_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='strict', + use_kerberos=False, + success=False) + + def test_uuid_bind_token_strict_with_unknown_bind(self): + token = self.token_dict['uuid_token_unknown_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='strict', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_required_with_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='required', + use_kerberos=True, + success=True) + + def test_uuid_bind_token_required_without_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='required', + use_kerberos=False, + success=False) + + def test_uuid_bind_token_required_with_unknown_bind(self): + token = self.token_dict['uuid_token_unknown_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='required', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_required_without_bind(self): + for use_kerberos in [True, False]: + self.assert_kerberos_bind(self.token_dict['uuid_token_default'], + bind_level='required', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_named_kerberos_with_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos=True, + success=True) + + def test_uuid_bind_token_named_kerberos_without_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos=False, + success=False) + + def test_uuid_bind_token_named_kerberos_with_unknown_bind(self): + token = self.token_dict['uuid_token_unknown_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='kerberos', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_named_kerberos_without_bind(self): + for use_kerberos in [True, False]: + self.assert_kerberos_bind(self.token_dict['uuid_token_default'], + bind_level='kerberos', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_named_kerberos_with_incorrect_ticket(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos='ronald@MCDONALDS.COM', + success=False) + + def test_uuid_bind_token_with_unknown_named_FOO(self): + token = self.token_dict['uuid_token_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='FOO', + use_kerberos=use_kerberos, + success=False) + + +class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest, + testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def __init__(self, *args, **kwargs): + super(V2CertDownloadMiddlewareTest, self).__init__(*args, **kwargs) + self.auth_version = 'v2.0' + self.fake_app = None + self.ca_path = '/v2.0/certificates/ca' + self.signing_path = '/v2.0/certificates/signing' + + def setUp(self): + super(V2CertDownloadMiddlewareTest, self).setUp( + auth_version=self.auth_version, + fake_app=self.fake_app) + self.base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.base_dir) + self.cert_dir = os.path.join(self.base_dir, 'certs') + os.makedirs(self.cert_dir, stat.S_IRWXU) + conf = { + 'signing_dir': self.cert_dir, + 'auth_version': self.auth_version, + } + self.set_middleware(conf=conf) + + # Usually we supply a signed_dir with pre-installed certificates, + # so invocation of /usr/bin/openssl succeeds. This time we give it + # an empty directory, so it fails. + def test_request_no_token_dummy(self): + cms._ensure_subprocess() + + self.requests.get("%s%s" % (BASE_URI, self.ca_path), + status_code=404) + url = "%s%s" % (BASE_URI, self.signing_path) + self.requests.get(url, status_code=404) + self.assertRaises(exceptions.CertificateConfigError, + self.middleware.verify_signed_token, + self.examples.SIGNED_TOKEN_SCOPED, + [self.examples.SIGNED_TOKEN_SCOPED_HASH]) + + def test_fetch_signing_cert(self): + data = 'FAKE CERT' + url = '%s%s' % (BASE_URI, self.signing_path) + self.requests.get(url, text=data) + self.middleware.fetch_signing_cert() + + with open(self.middleware.signing_cert_file_name, 'r') as f: + self.assertEqual(f.read(), data) + + self.assertLastPath("/testadmin%s" % self.signing_path) + + def test_fetch_signing_ca(self): + data = 'FAKE CA' + self.requests.get("%s%s" % (BASE_URI, self.ca_path), text=data) + self.middleware.fetch_ca_cert() + + with open(self.middleware.signing_ca_file_name, 'r') as f: + self.assertEqual(f.read(), data) + + self.assertLastPath("/testadmin%s" % self.ca_path) + + def test_prefix_trailing_slash(self): + del self.conf['identity_uri'] + self.conf['auth_protocol'] = 'https' + self.conf['auth_host'] = 'keystone.example.com' + self.conf['auth_port'] = 1234 + self.conf['auth_admin_prefix'] = '/newadmin/' + + self.requests.get("%s/newadmin%s" % (BASE_HOST, self.ca_path), + text='FAKECA') + url = "%s/newadmin%s" % (BASE_HOST, self.signing_path) + self.requests.get(url, text='FAKECERT') + + self.set_middleware(conf=self.conf) + + self.middleware.fetch_ca_cert() + + self.assertLastPath('/newadmin%s' % self.ca_path) + + self.middleware.fetch_signing_cert() + + self.assertLastPath('/newadmin%s' % self.signing_path) + + def test_without_prefix(self): + del self.conf['identity_uri'] + self.conf['auth_protocol'] = 'https' + self.conf['auth_host'] = 'keystone.example.com' + self.conf['auth_port'] = 1234 + self.conf['auth_admin_prefix'] = '' + + self.requests.get("%s%s" % (BASE_HOST, self.ca_path), text='FAKECA') + self.requests.get("%s%s" % (BASE_HOST, self.signing_path), + text='FAKECERT') + + self.set_middleware(conf=self.conf) + + self.middleware.fetch_ca_cert() + + self.assertLastPath(self.ca_path) + + self.middleware.fetch_signing_cert() + + self.assertLastPath(self.signing_path) + + +class V3CertDownloadMiddlewareTest(V2CertDownloadMiddlewareTest): + + def __init__(self, *args, **kwargs): + super(V3CertDownloadMiddlewareTest, self).__init__(*args, **kwargs) + self.auth_version = 'v3.0' + self.fake_app = v3FakeApp + self.ca_path = '/v3/OS-SIMPLE-CERT/ca' + self.signing_path = '/v3/OS-SIMPLE-CERT/certificates' + + +def network_error_response(method, uri, headers): + raise auth_token.NetworkError("Network connection error.") + + +class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + CommonAuthTokenMiddlewareTest, + testresources.ResourcedTestCase): + """v2 token specific tests. + + There are some differences between how the auth-token middleware handles + v2 and v3 tokens over and above the token formats, namely: + + - A v3 keystone server will auto scope a token to a user's default project + if no scope is specified. A v2 server assumes that the auth-token + middleware will do that. + - A v2 keystone server may issue a token without a catalog, even with a + tenant + + The tests below were originally part of the generic AuthTokenMiddlewareTest + class, but now, since they really are v2 specific, they are included here. + + """ + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def setUp(self): + super(v2AuthTokenMiddlewareTest, self).setUp() + + self.token_dict = { + 'uuid_token_default': self.examples.UUID_TOKEN_DEFAULT, + 'uuid_token_unscoped': self.examples.UUID_TOKEN_UNSCOPED, + 'uuid_token_bind': self.examples.UUID_TOKEN_BIND, + 'uuid_token_unknown_bind': self.examples.UUID_TOKEN_UNKNOWN_BIND, + 'signed_token_scoped': self.examples.SIGNED_TOKEN_SCOPED, + 'signed_token_scoped_pkiz': self.examples.SIGNED_TOKEN_SCOPED_PKIZ, + 'signed_token_scoped_hash': self.examples.SIGNED_TOKEN_SCOPED_HASH, + 'signed_token_scoped_hash_sha256': + self.examples.SIGNED_TOKEN_SCOPED_HASH_SHA256, + 'signed_token_scoped_expired': + self.examples.SIGNED_TOKEN_SCOPED_EXPIRED, + 'revoked_token': self.examples.REVOKED_TOKEN, + 'revoked_token_pkiz': self.examples.REVOKED_TOKEN_PKIZ, + 'revoked_token_pkiz_hash': + self.examples.REVOKED_TOKEN_PKIZ_HASH, + 'revoked_token_hash': self.examples.REVOKED_TOKEN_HASH, + 'revoked_token_hash_sha256': + self.examples.REVOKED_TOKEN_HASH_SHA256, + } + + self.requests.get("%s/" % BASE_URI, + text=VERSION_LIST_v2, + status_code=300) + + self.requests.post("%s/v2.0/tokens" % BASE_URI, + text=FAKE_ADMIN_TOKEN) + + self.requests.get("%s/v2.0/tokens/revoked" % BASE_URI, + text=self.examples.SIGNED_REVOCATION_LIST) + + for token in (self.examples.UUID_TOKEN_DEFAULT, + self.examples.UUID_TOKEN_UNSCOPED, + self.examples.UUID_TOKEN_BIND, + self.examples.UUID_TOKEN_UNKNOWN_BIND, + self.examples.UUID_TOKEN_NO_SERVICE_CATALOG, + self.examples.SIGNED_TOKEN_SCOPED_KEY, + self.examples.SIGNED_TOKEN_SCOPED_PKIZ_KEY,): + text = self.examples.JSON_TOKEN_RESPONSES[token] + self.requests.get('%s/v2.0/tokens/%s' % (BASE_URI, token), + text=text) + + self.requests.get('%s/v2.0/tokens/%s' % (BASE_URI, ERROR_TOKEN), + text=network_error_response) + + self.set_middleware() + + def assert_unscoped_default_tenant_auto_scopes(self, token): + """Unscoped v2 requests with a default tenant should "auto-scope." + + The implied scope is the user's tenant ID. + + """ + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertEqual(body, [FakeApp.SUCCESS]) + self.assertIn('keystone.token_info', req.environ) + + def assert_valid_last_url(self, token_id): + self.assertLastPath("/testadmin/v2.0/tokens/%s" % token_id) + + def test_default_tenant_uuid_token(self): + self.assert_unscoped_default_tenant_auto_scopes( + self.examples.UUID_TOKEN_DEFAULT) + + def test_default_tenant_signed_token(self): + self.assert_unscoped_default_tenant_auto_scopes( + self.examples.SIGNED_TOKEN_SCOPED) + + def assert_unscoped_token_receives_401(self, token): + """Unscoped requests with no default tenant ID should be rejected.""" + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def test_unscoped_uuid_token_receives_401(self): + self.assert_unscoped_token_receives_401( + self.examples.UUID_TOKEN_UNSCOPED) + + def test_unscoped_pki_token_receives_401(self): + self.assert_unscoped_token_receives_401( + self.examples.SIGNED_TOKEN_UNSCOPED) + + def test_request_prevent_service_catalog_injection(self): + req = webob.Request.blank('/') + req.headers['X-Service-Catalog'] = '[]' + req.headers['X-Auth-Token'] = ( + self.examples.UUID_TOKEN_NO_SERVICE_CATALOG) + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertFalse(req.headers.get('X-Service-Catalog')) + self.assertEqual(body, [FakeApp.SUCCESS]) + + +class CrossVersionAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_valid_uuid_request_forced_to_2_0(self): + """Test forcing auth_token to use lower api version. + + By installing the v3 http hander, auth_token will be get + a version list that looks like a v3 server - from which it + would normally chose v3.0 as the auth version. However, here + we specify v2.0 in the configuration - which should force + auth_token to use that version instead. + + """ + conf = { + 'signing_dir': client_fixtures.CERTDIR, + 'auth_version': 'v2.0' + } + + self.requests.get('%s/' % BASE_URI, + text=VERSION_LIST_v3, + status_code=300) + + self.requests.post('%s/v2.0/tokens' % BASE_URI, text=FAKE_ADMIN_TOKEN) + + token = self.examples.UUID_TOKEN_DEFAULT + url = '%s/v2.0/tokens/%s' % (BASE_URI, token) + response_body = self.examples.JSON_TOKEN_RESPONSES[token] + self.requests.get(url, text=response_body) + + self.set_middleware(conf=conf) + + # This tests will only work is auth_token has chosen to use the + # lower, v2, api version + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.examples.UUID_TOKEN_DEFAULT + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertLastPath("/testadmin/v2.0/tokens/%s" % + self.examples.UUID_TOKEN_DEFAULT) + + +class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + CommonAuthTokenMiddlewareTest, + testresources.ResourcedTestCase): + """Test auth_token middleware with v3 tokens. + + Re-execute the AuthTokenMiddlewareTest class tests, but with the + auth_token middleware configured to expect v3 tokens back from + a keystone server. + + This is done by configuring the AuthTokenMiddlewareTest class via + its Setup(), passing in v3 style data that will then be used by + the tests themselves. This approach has been used to ensure we + really are running the same tests for both v2 and v3 tokens. + + There a few additional specific test for v3 only: + + - We allow an unscoped token to be validated (as unscoped), where + as for v2 tokens, the auth_token middleware is expected to try and + auto-scope it (and fail if there is no default tenant) + - Domain scoped tokens + + Since we don't specify an auth version for auth_token to use, by + definition we are thefore implicitely testing that it will use + the highest available auth version, i.e. v3.0 + + """ + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def setUp(self): + super(v3AuthTokenMiddlewareTest, self).setUp( + auth_version='v3.0', + fake_app=v3FakeApp) + + self.token_dict = { + 'uuid_token_default': self.examples.v3_UUID_TOKEN_DEFAULT, + 'uuid_token_unscoped': self.examples.v3_UUID_TOKEN_UNSCOPED, + 'uuid_token_bind': self.examples.v3_UUID_TOKEN_BIND, + 'uuid_token_unknown_bind': + self.examples.v3_UUID_TOKEN_UNKNOWN_BIND, + 'signed_token_scoped': self.examples.SIGNED_v3_TOKEN_SCOPED, + 'signed_token_scoped_pkiz': + self.examples.SIGNED_v3_TOKEN_SCOPED_PKIZ, + 'signed_token_scoped_hash': + self.examples.SIGNED_v3_TOKEN_SCOPED_HASH, + 'signed_token_scoped_hash_sha256': + self.examples.SIGNED_v3_TOKEN_SCOPED_HASH_SHA256, + 'signed_token_scoped_expired': + self.examples.SIGNED_TOKEN_SCOPED_EXPIRED, + 'revoked_token': self.examples.REVOKED_v3_TOKEN, + 'revoked_token_pkiz': self.examples.REVOKED_v3_TOKEN_PKIZ, + 'revoked_token_hash': self.examples.REVOKED_v3_TOKEN_HASH, + 'revoked_token_hash_sha256': + self.examples.REVOKED_v3_TOKEN_HASH_SHA256, + 'revoked_token_pkiz_hash': + self.examples.REVOKED_v3_PKIZ_TOKEN_HASH, + } + + self.requests.get(BASE_URI, text=VERSION_LIST_v3, status_code=300) + + # TODO(jamielennox): auth_token middleware uses a v2 admin token + # regardless of the auth_version that is set. + self.requests.post('%s/v2.0/tokens' % BASE_URI, text=FAKE_ADMIN_TOKEN) + + # TODO(jamielennox): there is no v3 revocation url yet, it uses v2 + self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI, + text=self.examples.SIGNED_REVOCATION_LIST) + + self.requests.get('%s/v3/auth/tokens' % BASE_URI, + text=self.token_response) + + self.set_middleware() + + def token_response(self, request, context): + auth_id = request.headers.get('X-Auth-Token') + token_id = request.headers.get('X-Subject-Token') + self.assertEqual(auth_id, FAKE_ADMIN_TOKEN_ID) + + response = "" + + if token_id == ERROR_TOKEN: + raise auth_token.NetworkError("Network connection error.") + + try: + response = self.examples.JSON_TOKEN_RESPONSES[token_id] + except KeyError: + context.status_code = 404 + + return response + + def assert_valid_last_url(self, token_id): + self.assertLastPath('/testadmin/v3/auth/tokens') + + def test_valid_unscoped_uuid_request(self): + # Remove items that won't be in an unscoped token + delta_expected_env = { + 'HTTP_X_PROJECT_ID': None, + 'HTTP_X_PROJECT_NAME': None, + 'HTTP_X_PROJECT_DOMAIN_ID': None, + 'HTTP_X_PROJECT_DOMAIN_NAME': None, + 'HTTP_X_TENANT_ID': None, + 'HTTP_X_TENANT_NAME': None, + 'HTTP_X_ROLES': '', + 'HTTP_X_TENANT': None, + 'HTTP_X_ROLE': '', + } + self.set_middleware(expected_env=delta_expected_env) + self.assert_valid_request_200(self.examples.v3_UUID_TOKEN_UNSCOPED, + with_catalog=False) + self.assertLastPath('/testadmin/v3/auth/tokens') + + def test_domain_scoped_uuid_request(self): + # Modify items compared to default token for a domain scope + delta_expected_env = { + 'HTTP_X_DOMAIN_ID': 'domain_id1', + 'HTTP_X_DOMAIN_NAME': 'domain_name1', + 'HTTP_X_PROJECT_ID': None, + 'HTTP_X_PROJECT_NAME': None, + 'HTTP_X_PROJECT_DOMAIN_ID': None, + 'HTTP_X_PROJECT_DOMAIN_NAME': None, + 'HTTP_X_TENANT_ID': None, + 'HTTP_X_TENANT_NAME': None, + 'HTTP_X_TENANT': None + } + self.set_middleware(expected_env=delta_expected_env) + self.assert_valid_request_200( + self.examples.v3_UUID_TOKEN_DOMAIN_SCOPED) + self.assertLastPath('/testadmin/v3/auth/tokens') + + def test_gives_v2_catalog(self): + self.set_middleware() + req = self.assert_valid_request_200( + self.examples.SIGNED_v3_TOKEN_SCOPED) + + catalog = jsonutils.loads(req.headers['X-Service-Catalog']) + + for service in catalog: + for endpoint in service['endpoints']: + # no point checking everything, just that it's in v2 format + self.assertIn('adminURL', endpoint) + self.assertIn('publicURL', endpoint) + self.assertIn('adminURL', endpoint) + + +class TokenEncodingTest(testtools.TestCase): + def test_unquoted_token(self): + self.assertEqual('foo%20bar', auth_token.safe_quote('foo bar')) + + def test_quoted_token(self): + self.assertEqual('foo%20bar', auth_token.safe_quote('foo%20bar')) + + +class TokenExpirationTest(BaseAuthTokenMiddlewareTest): + def setUp(self): + super(TokenExpirationTest, self).setUp() + self.now = timeutils.utcnow() + self.delta = datetime.timedelta(hours=1) + self.one_hour_ago = timeutils.isotime(self.now - self.delta, + subsecond=True) + self.one_hour_earlier = timeutils.isotime(self.now + self.delta, + subsecond=True) + + def create_v2_token_fixture(self, expires=None): + v2_fixture = { + 'access': { + 'token': { + 'id': 'blah', + 'expires': expires or self.one_hour_earlier, + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + } + + return v2_fixture + + def create_v3_token_fixture(self, expires=None): + + v3_fixture = { + 'token': { + 'expires_at': expires or self.one_hour_earlier, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + } + + return v3_fixture + + def test_no_data(self): + data = {} + self.assertRaises(auth_token.InvalidUserToken, + auth_token.confirm_token_not_expired, + data) + + def test_bad_data(self): + data = {'my_happy_token_dict': 'woo'} + self.assertRaises(auth_token.InvalidUserToken, + auth_token.confirm_token_not_expired, + data) + + def test_v2_token_not_expired(self): + data = self.create_v2_token_fixture() + expected_expires = data['access']['token']['expires'] + actual_expires = auth_token.confirm_token_not_expired(data) + self.assertEqual(actual_expires, expected_expires) + + def test_v2_token_expired(self): + data = self.create_v2_token_fixture(expires=self.one_hour_ago) + self.assertRaises(auth_token.InvalidUserToken, + auth_token.confirm_token_not_expired, + data) + + def test_v2_token_with_timezone_offset_not_expired(self): + self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z')) + data = self.create_v2_token_fixture( + expires='2000-01-01T00:05:10.000123-05:00') + expected_expires = '2000-01-01T05:05:10.000123Z' + actual_expires = auth_token.confirm_token_not_expired(data) + self.assertEqual(actual_expires, expected_expires) + + def test_v2_token_with_timezone_offset_expired(self): + self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z')) + data = self.create_v2_token_fixture( + expires='2000-01-01T00:05:10.000123+05:00') + data['access']['token']['expires'] = '2000-01-01T00:05:10.000123+05:00' + self.assertRaises(auth_token.InvalidUserToken, + auth_token.confirm_token_not_expired, + data) + + def test_v3_token_not_expired(self): + data = self.create_v3_token_fixture() + expected_expires = data['token']['expires_at'] + actual_expires = auth_token.confirm_token_not_expired(data) + self.assertEqual(actual_expires, expected_expires) + + def test_v3_token_expired(self): + data = self.create_v3_token_fixture(expires=self.one_hour_ago) + self.assertRaises(auth_token.InvalidUserToken, + auth_token.confirm_token_not_expired, + data) + + def test_v3_token_with_timezone_offset_not_expired(self): + self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z')) + data = self.create_v3_token_fixture( + expires='2000-01-01T00:05:10.000123-05:00') + expected_expires = '2000-01-01T05:05:10.000123Z' + + actual_expires = auth_token.confirm_token_not_expired(data) + self.assertEqual(actual_expires, expected_expires) + + def test_v3_token_with_timezone_offset_expired(self): + self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z')) + data = self.create_v3_token_fixture( + expires='2000-01-01T00:05:10.000123+05:00') + self.assertRaises(auth_token.InvalidUserToken, + auth_token.confirm_token_not_expired, + data) + + def test_cached_token_not_expired(self): + token = 'mytoken' + data = 'this_data' + self.set_middleware() + self.middleware._token_cache.initialize({}) + some_time_later = timeutils.strtime(at=(self.now + self.delta)) + expires = some_time_later + self.middleware._token_cache.store(token, data, expires) + self.assertEqual(self.middleware._token_cache._cache_get(token), data) + + def test_cached_token_not_expired_with_old_style_nix_timestamp(self): + """Ensure we cannot retrieve a token from the cache. + + Getting a token from the cache should return None when the token data + in the cache stores the expires time as a \*nix style timestamp. + + """ + token = 'mytoken' + data = 'this_data' + self.set_middleware() + token_cache = self.middleware._token_cache + token_cache.initialize({}) + some_time_later = self.now + self.delta + # Store a unix timestamp in the cache. + expires = calendar.timegm(some_time_later.timetuple()) + token_cache.store(token, data, expires) + self.assertIsNone(token_cache._cache_get(token)) + + def test_cached_token_expired(self): + token = 'mytoken' + data = 'this_data' + self.set_middleware() + self.middleware._token_cache.initialize({}) + some_time_earlier = timeutils.strtime(at=(self.now - self.delta)) + expires = some_time_earlier + self.middleware._token_cache.store(token, data, expires) + self.assertThat(lambda: self.middleware._token_cache._cache_get(token), + matchers.raises(auth_token.InvalidUserToken)) + + def test_cached_token_with_timezone_offset_not_expired(self): + token = 'mytoken' + data = 'this_data' + self.set_middleware() + self.middleware._token_cache.initialize({}) + timezone_offset = datetime.timedelta(hours=2) + some_time_later = self.now - timezone_offset + self.delta + expires = timeutils.strtime(some_time_later) + '-02:00' + self.middleware._token_cache.store(token, data, expires) + self.assertEqual(self.middleware._token_cache._cache_get(token), data) + + def test_cached_token_with_timezone_offset_expired(self): + token = 'mytoken' + data = 'this_data' + self.set_middleware() + self.middleware._token_cache.initialize({}) + timezone_offset = datetime.timedelta(hours=2) + some_time_earlier = self.now - timezone_offset - self.delta + expires = timeutils.strtime(some_time_earlier) + '-02:00' + self.middleware._token_cache.store(token, data, expires) + self.assertThat(lambda: self.middleware._token_cache._cache_get(token), + matchers.raises(auth_token.InvalidUserToken)) + + +class CatalogConversionTests(BaseAuthTokenMiddlewareTest): + + PUBLIC_URL = 'http://server:5000/v2.0' + ADMIN_URL = 'http://admin:35357/v2.0' + INTERNAL_URL = 'http://internal:5000/v2.0' + + REGION_ONE = 'RegionOne' + REGION_TWO = 'RegionTwo' + REGION_THREE = 'RegionThree' + + def test_basic_convert(self): + token = fixture.V3Token() + s = token.add_service(type='identity') + s.add_standard_endpoints(public=self.PUBLIC_URL, + admin=self.ADMIN_URL, + internal=self.INTERNAL_URL, + region=self.REGION_ONE) + + auth_ref = access.AccessInfo.factory(body=token) + catalog_data = auth_ref.service_catalog.get_data() + catalog = auth_token._v3_to_v2_catalog(catalog_data) + + self.assertEqual(1, len(catalog)) + service = catalog[0] + self.assertEqual(1, len(service['endpoints'])) + endpoints = service['endpoints'][0] + + self.assertEqual('identity', service['type']) + self.assertEqual(4, len(endpoints)) + self.assertEqual(self.PUBLIC_URL, endpoints['publicURL']) + self.assertEqual(self.ADMIN_URL, endpoints['adminURL']) + self.assertEqual(self.INTERNAL_URL, endpoints['internalURL']) + self.assertEqual(self.REGION_ONE, endpoints['region']) + + def test_multi_region(self): + token = fixture.V3Token() + s = token.add_service(type='identity') + + s.add_endpoint('internal', self.INTERNAL_URL, region=self.REGION_ONE) + s.add_endpoint('public', self.PUBLIC_URL, region=self.REGION_TWO) + s.add_endpoint('admin', self.ADMIN_URL, region=self.REGION_THREE) + + auth_ref = access.AccessInfo.factory(body=token) + catalog_data = auth_ref.service_catalog.get_data() + catalog = auth_token._v3_to_v2_catalog(catalog_data) + + self.assertEqual(1, len(catalog)) + service = catalog[0] + + # the 3 regions will come through as 3 separate endpoints + expected = [{'internalURL': self.INTERNAL_URL, + 'region': self.REGION_ONE}, + {'publicURL': self.PUBLIC_URL, + 'region': self.REGION_TWO}, + {'adminURL': self.ADMIN_URL, + 'region': self.REGION_THREE}] + + self.assertEqual('identity', service['type']) + self.assertEqual(3, len(service['endpoints'])) + for e in expected: + self.assertIn(e, expected) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/test_base.py b/keystoneclient/tests/unit/test_base.py new file mode 100644 index 0000000..2e7fc5e --- /dev/null +++ b/keystoneclient/tests/unit/test_base.py @@ -0,0 +1,161 @@ +# 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 import base +from keystoneclient.tests.unit import utils +from keystoneclient.v2_0 import client +from keystoneclient.v2_0 import roles + + +class HumanReadable(base.Resource): + HUMAN_ID = True + + +class BaseTest(utils.TestCase): + + def test_resource_repr(self): + r = base.Resource(None, dict(foo="bar", baz="spam")) + self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>") + + def test_getid(self): + self.assertEqual(base.getid(4), 4) + + class TmpObject(object): + id = 4 + self.assertEqual(base.getid(TmpObject), 4) + + def test_resource_lazy_getattr(self): + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url='http://127.0.0.1:5000', + endpoint='http://127.0.0.1:5000') + + self.client._adapter.get = self.mox.CreateMockAnything() + self.client._adapter.get('/OS-KSADM/roles/1').AndRaise(AttributeError) + self.mox.ReplayAll() + + f = roles.Role(self.client.roles, {'id': 1, 'name': 'Member'}) + self.assertEqual(f.name, 'Member') + + # Missing stuff still fails after a second get + self.assertRaises(AttributeError, getattr, f, 'blahblah') + + def test_eq(self): + # Two resources of the same type with the same id: equal + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertEqual(r1, r2) + + # Two resoruces of different types: never equal + r1 = base.Resource(None, {'id': 1}) + r2 = roles.Role(None, {'id': 1}) + self.assertNotEqual(r1, r2) + + # Two resources with no ID: equal if their info is equal + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) + self.assertEqual(r1, r2) + + r1 = base.Resource(None, {'id': 1}) + self.assertNotEqual(r1, object()) + self.assertNotEqual(r1, {'id': 1}) + + def test_human_id(self): + r = base.Resource(None, {"name": "1 of !"}) + self.assertIsNone(r.human_id) + r = HumanReadable(None, {"name": "1 of !"}) + self.assertEqual(r.human_id, "1-of") + + +class ManagerTest(utils.TestCase): + body = {"hello": {"hi": 1}} + url = "/test-url" + + def setUp(self): + super(ManagerTest, self).setUp() + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url='http://127.0.0.1:5000', + endpoint='http://127.0.0.1:5000') + self.mgr = base.Manager(self.client) + self.mgr.resource_class = base.Resource + + def test_api(self): + self.assertEqual(self.mgr.api, self.client) + + def test_get(self): + self.client.get = self.mox.CreateMockAnything() + self.client.get(self.url).AndReturn((None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._get(self.url, "hello") + self.assertEqual(rsrc.hi, 1) + + def test_post(self): + self.client.post = self.mox.CreateMockAnything() + self.client.post(self.url, body=self.body).AndReturn((None, self.body)) + self.client.post(self.url, body=self.body).AndReturn((None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._post(self.url, self.body, "hello") + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._post(self.url, self.body, "hello", return_raw=True) + self.assertEqual(rsrc["hi"], 1) + + def test_put(self): + self.client.put = self.mox.CreateMockAnything() + self.client.put(self.url, body=self.body).AndReturn((None, self.body)) + self.client.put(self.url, body=self.body).AndReturn((None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._put(self.url, self.body, "hello") + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._put(self.url, self.body) + self.assertEqual(rsrc.hello["hi"], 1) + + def test_patch(self): + self.client.patch = self.mox.CreateMockAnything() + self.client.patch(self.url, body=self.body).AndReturn( + (None, self.body)) + self.client.patch(self.url, body=self.body).AndReturn( + (None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._patch(self.url, self.body, "hello") + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._patch(self.url, self.body) + self.assertEqual(rsrc.hello["hi"], 1) + + def test_update(self): + self.client.patch = self.mox.CreateMockAnything() + self.client.put = self.mox.CreateMockAnything() + self.client.patch( + self.url, body=self.body, management=False).AndReturn((None, + self.body)) + self.client.put(self.url, body=None, management=True).AndReturn( + (None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._update( + self.url, body=self.body, response_key="hello", method="PATCH", + management=False) + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._update( + self.url, body=None, response_key="hello", method="PUT", + management=True) + self.assertEqual(rsrc.hi, 1) diff --git a/keystoneclient/tests/unit/test_cms.py b/keystoneclient/tests/unit/test_cms.py new file mode 100644 index 0000000..019730d --- /dev/null +++ b/keystoneclient/tests/unit/test_cms.py @@ -0,0 +1,160 @@ +# 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 errno +import os +import subprocess + +import mock +import testresources +from testtools import matchers + +from keystoneclient.common import cms +from keystoneclient import exceptions +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit import utils + + +class CMSTest(utils.TestCase, testresources.ResourcedTestCase): + + """Unit tests for the keystoneclient.common.cms module.""" + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def __init__(self, *args, **kwargs): + super(CMSTest, self).__init__(*args, **kwargs) + process = subprocess.Popen(['openssl', 'version'], + stdout=subprocess.PIPE) + out, err = process.communicate() + # Example output: 'OpenSSL 0.9.8za 5 Jun 2014' + openssl_version = out.split()[1] + + if err or openssl_version.startswith(b'0'): + raise Exception('Your version of OpenSSL is not supported. ' + 'You will need to update it to 1.0 or later.') + + def test_cms_verify(self): + self.assertRaises(exceptions.CertificateConfigError, + cms.cms_verify, + 'data', + 'no_exist_cert_file', + 'no_exist_ca_file') + + def test_token_tocms_to_token(self): + with open(os.path.join(client_fixtures.CMSDIR, + 'auth_token_scoped.pem')) as f: + AUTH_TOKEN_SCOPED_CMS = f.read() + + self.assertEqual(cms.token_to_cms(self.examples.SIGNED_TOKEN_SCOPED), + AUTH_TOKEN_SCOPED_CMS) + + tok = cms.cms_to_token(cms.token_to_cms( + self.examples.SIGNED_TOKEN_SCOPED)) + self.assertEqual(tok, self.examples.SIGNED_TOKEN_SCOPED) + + def test_asn1_token(self): + self.assertTrue(cms.is_asn1_token(self.examples.SIGNED_TOKEN_SCOPED)) + self.assertFalse(cms.is_asn1_token('FOOBAR')) + + def test_cms_sign_token_no_files(self): + self.assertRaises(subprocess.CalledProcessError, + cms.cms_sign_token, + self.examples.TOKEN_SCOPED_DATA, + '/no/such/file', '/no/such/key') + + def test_cms_sign_token_no_files_pkiz(self): + self.assertRaises(subprocess.CalledProcessError, + cms.pkiz_sign, + self.examples.TOKEN_SCOPED_DATA, + '/no/such/file', '/no/such/key') + + def test_cms_sign_token_success(self): + self.assertTrue( + cms.pkiz_sign(self.examples.TOKEN_SCOPED_DATA, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_KEY_FILE)) + + def test_cms_verify_token_no_files(self): + self.assertRaises(exceptions.CertificateConfigError, + cms.cms_verify, + self.examples.SIGNED_TOKEN_SCOPED, + '/no/such/file', '/no/such/key') + + def test_cms_verify_token_no_oserror(self): + def raise_OSError(*args): + e = OSError() + e.errno = errno.EPIPE + raise e + + with mock.patch('subprocess.Popen.communicate', new=raise_OSError): + try: + cms.cms_verify("x", '/no/such/file', '/no/such/key') + except exceptions.CertificateConfigError as e: + self.assertIn('/no/such/file', e.output) + self.assertIn('Hit OSError ', e.output) + else: + self.fail('Expected exceptions.CertificateConfigError') + + def test_cms_verify_token_scoped(self): + cms_content = cms.token_to_cms(self.examples.SIGNED_TOKEN_SCOPED) + self.assertTrue(cms.cms_verify(cms_content, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_CA_FILE)) + + def test_cms_verify_token_scoped_expired(self): + cms_content = cms.token_to_cms( + self.examples.SIGNED_TOKEN_SCOPED_EXPIRED) + self.assertTrue(cms.cms_verify(cms_content, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_CA_FILE)) + + def test_cms_verify_token_unscoped(self): + cms_content = cms.token_to_cms(self.examples.SIGNED_TOKEN_UNSCOPED) + self.assertTrue(cms.cms_verify(cms_content, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_CA_FILE)) + + def test_cms_verify_token_v3_scoped(self): + cms_content = cms.token_to_cms(self.examples.SIGNED_v3_TOKEN_SCOPED) + self.assertTrue(cms.cms_verify(cms_content, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_CA_FILE)) + + def test_cms_hash_token_no_token_id(self): + token_id = None + self.assertThat(cms.cms_hash_token(token_id), matchers.Is(None)) + + def test_cms_hash_token_not_pki(self): + """If the token_id is not a PKI token then it returns the token_id.""" + token = 'something' + self.assertFalse(cms.is_asn1_token(token)) + self.assertThat(cms.cms_hash_token(token), matchers.Is(token)) + + def test_cms_hash_token_default_md5(self): + """The default hash method is md5.""" + token = self.examples.SIGNED_TOKEN_SCOPED + token_id_default = cms.cms_hash_token(token) + token_id_md5 = cms.cms_hash_token(token, mode='md5') + self.assertThat(token_id_default, matchers.Equals(token_id_md5)) + # md5 hash is 32 chars. + self.assertThat(token_id_default, matchers.HasLength(32)) + + def test_cms_hash_token_sha256(self): + """Can also hash with sha256.""" + token = self.examples.SIGNED_TOKEN_SCOPED + token_id = cms.cms_hash_token(token, mode='sha256') + # sha256 hash is 64 chars. + self.assertThat(token_id, matchers.HasLength(64)) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/test_discovery.py b/keystoneclient/tests/unit/test_discovery.py new file mode 100644 index 0000000..6c208a3 --- /dev/null +++ b/keystoneclient/tests/unit/test_discovery.py @@ -0,0 +1,802 @@ +# 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 re +import uuid + +from oslo_serialization import jsonutils +import six +from testtools import matchers + +from keystoneclient import _discover +from keystoneclient.auth import token_endpoint +from keystoneclient import client +from keystoneclient import discover +from keystoneclient import exceptions +from keystoneclient import fixture +from keystoneclient import session +from keystoneclient.tests.unit import utils +from keystoneclient.v2_0 import client as v2_client +from keystoneclient.v3 import client as v3_client + + +BASE_HOST = 'http://keystone.example.com' +BASE_URL = "%s:5000/" % BASE_HOST +UPDATED = '2013-03-06T00:00:00Z' + +TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "adminURL": "%s:8774/v1.0" % BASE_HOST, + "region": "RegionOne", + "internalURL": "%s://127.0.0.1:8774/v1.0" % BASE_HOST, + "publicURL": "%s:8774/v1.0/" % BASE_HOST + }], + "type": "nova_compat", + "name": "nova_compat" +}, { + "endpoints": [{ + "adminURL": "http://nova/novapi/admin", + "region": "RegionOne", + "internalURL": "http://nova/novapi/internal", + "publicURL": "http://nova/novapi/public" + }], + "type": "compute", + "name": "nova" +}, { + "endpoints": [{ + "adminURL": "http://glance/glanceapi/admin", + "region": "RegionOne", + "internalURL": "http://glance/glanceapi/internal", + "publicURL": "http://glance/glanceapi/public" + }], + "type": "image", + "name": "glance" +}, { + "endpoints": [{ + "adminURL": "%s:35357/v2.0" % BASE_HOST, + "region": "RegionOne", + "internalURL": "%s:5000/v2.0" % BASE_HOST, + "publicURL": "%s:5000/v2.0" % BASE_HOST + }], + "type": "identity", + "name": "keystone" +}, { + "endpoints": [{ + "adminURL": "http://swift/swiftapi/admin", + "region": "RegionOne", + "internalURL": "http://swift/swiftapi/internal", + "publicURL": "http://swift/swiftapi/public" + }], + "type": "object-store", + "name": "swift" +}] + +V2_URL = "%sv2.0" % BASE_URL +V2_VERSION = fixture.V2Discovery(V2_URL) +V2_VERSION.updated_str = UPDATED + +V2_AUTH_RESPONSE = jsonutils.dumps({ + "access": { + "token": { + "expires": "2020-01-01T00:00:10.000123Z", + "id": 'fakeToken', + "tenant": { + "id": '1' + }, + }, + "user": { + "id": 'test' + }, + "serviceCatalog": TEST_SERVICE_CATALOG, + }, +}) + +V3_URL = "%sv3" % BASE_URL +V3_VERSION = fixture.V3Discovery(V3_URL) +V3_MEDIA_TYPES = V3_VERSION.media_types +V3_VERSION.updated_str = UPDATED + +V3_TOKEN = six.u('3e2813b7ba0b4006840c3825860b86ed'), +V3_AUTH_RESPONSE = jsonutils.dumps({ + "token": { + "methods": [ + "token", + "password" + ], + + "expires_at": "2020-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": '1', + "name": 'test-domain' + }, + "id": '1', + "name": 'test-project' + }, + "user": { + "domain": { + "id": '1', + "name": 'test-domain' + }, + "id": '1', + "name": 'test-user' + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + }, +}) + +CINDER_EXAMPLES = { + "versions": [ + { + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "id": "v1.0", + "links": [ + { + "href": "%sv1/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "CURRENT", + "updated": "2012-11-21T11:33:21Z", + "id": "v2.0", + "links": [ + { + "href": "%sv2/" % BASE_URL, + "rel": "self" + } + ] + } + ] +} + +GLANCE_EXAMPLES = { + "versions": [ + { + "status": "CURRENT", + "id": "v2.2", + "links": [ + { + "href": "%sv2/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.1", + "links": [ + { + "href": "%sv2/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.0", + "links": [ + { + "href": "%sv2/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "CURRENT", + "id": "v1.1", + "links": [ + { + "href": "%sv1/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.0", + "links": [ + { + "href": "%sv1/" % BASE_URL, + "rel": "self" + } + ] + } + ] +} + + +def _create_version_list(versions): + return jsonutils.dumps({'versions': {'values': versions}}) + + +def _create_single_version(version): + return jsonutils.dumps({'version': version}) + + +V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) +V2_VERSION_LIST = _create_version_list([V2_VERSION]) + +V3_VERSION_ENTRY = _create_single_version(V3_VERSION) +V2_VERSION_ENTRY = _create_single_version(V2_VERSION) + + +class AvailableVersionsTests(utils.TestCase): + + def test_available_versions_basics(self): + examples = {'keystone': V3_VERSION_LIST, + 'cinder': jsonutils.dumps(CINDER_EXAMPLES), + 'glance': jsonutils.dumps(GLANCE_EXAMPLES)} + + for path, text in six.iteritems(examples): + url = "%s%s" % (BASE_URL, path) + + self.requests.get(url, status_code=300, text=text) + versions = discover.available_versions(url) + + for v in versions: + for n in ('id', 'status', 'links'): + msg = '%s missing from %s version data' % (n, path) + self.assertThat(v, matchers.Annotate(msg, + matchers.Contains(n))) + + def test_available_versions_individual(self): + self.requests.get(V3_URL, status_code=200, text=V3_VERSION_ENTRY) + + versions = discover.available_versions(V3_URL) + + for v in versions: + self.assertEqual(v['id'], 'v3.0') + self.assertEqual(v['status'], 'stable') + self.assertIn('media-types', v) + self.assertIn('links', v) + + def test_available_keystone_data(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + versions = discover.available_versions(BASE_URL) + self.assertEqual(2, len(versions)) + + for v in versions: + self.assertIn(v['id'], ('v2.0', 'v3.0')) + self.assertEqual(v['updated'], UPDATED) + self.assertEqual(v['status'], 'stable') + + if v['id'] == 'v3.0': + self.assertEqual(v['media-types'], V3_MEDIA_TYPES) + + def test_available_cinder_data(self): + text = jsonutils.dumps(CINDER_EXAMPLES) + self.requests.get(BASE_URL, status_code=300, text=text) + + versions = discover.available_versions(BASE_URL) + self.assertEqual(2, len(versions)) + + for v in versions: + self.assertEqual(v['status'], 'CURRENT') + if v['id'] == 'v1.0': + self.assertEqual(v['updated'], '2012-01-04T11:33:21Z') + elif v['id'] == 'v2.0': + self.assertEqual(v['updated'], '2012-11-21T11:33:21Z') + else: + self.fail("Invalid version found") + + def test_available_glance_data(self): + text = jsonutils.dumps(GLANCE_EXAMPLES) + self.requests.get(BASE_URL, status_code=200, text=text) + + versions = discover.available_versions(BASE_URL) + self.assertEqual(5, len(versions)) + + for v in versions: + if v['id'] in ('v2.2', 'v1.1'): + self.assertEqual(v['status'], 'CURRENT') + elif v['id'] in ('v2.1', 'v2.0', 'v1.0'): + self.assertEqual(v['status'], 'SUPPORTED') + else: + self.fail("Invalid version found") + + +class ClientDiscoveryTests(utils.TestCase): + + def assertCreatesV3(self, **kwargs): + self.requests.post('%s/auth/tokens' % V3_URL, + text=V3_AUTH_RESPONSE, + headers={'X-Subject-Token': V3_TOKEN}) + + kwargs.setdefault('username', 'foo') + kwargs.setdefault('password', 'bar') + keystone = client.Client(**kwargs) + self.assertIsInstance(keystone, v3_client.Client) + return keystone + + def assertCreatesV2(self, **kwargs): + self.requests.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE) + + kwargs.setdefault('username', 'foo') + kwargs.setdefault('password', 'bar') + keystone = client.Client(**kwargs) + self.assertIsInstance(keystone, v2_client.Client) + return keystone + + def assertVersionNotAvailable(self, **kwargs): + kwargs.setdefault('username', 'foo') + kwargs.setdefault('password', 'bar') + + self.assertRaises(exceptions.VersionNotAvailable, + client.Client, **kwargs) + + def assertDiscoveryFailure(self, **kwargs): + kwargs.setdefault('username', 'foo') + kwargs.setdefault('password', 'bar') + + self.assertRaises(exceptions.DiscoveryFailure, + client.Client, **kwargs) + + def test_discover_v3(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + self.assertCreatesV3(auth_url=BASE_URL) + + def test_discover_v2(self): + self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST) + self.requests.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE) + + self.assertCreatesV2(auth_url=BASE_URL) + + def test_discover_endpoint_v2(self): + self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST) + self.assertCreatesV2(endpoint=BASE_URL, token='fake-token') + + def test_discover_endpoint_v3(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + self.assertCreatesV3(endpoint=BASE_URL, token='fake-token') + + def test_discover_invalid_major_version(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + self.assertVersionNotAvailable(auth_url=BASE_URL, version=5) + + def test_discover_200_response_fails(self): + self.requests.get(BASE_URL, text='ok') + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_discover_minor_greater_than_available_fails(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + self.assertVersionNotAvailable(endpoint=BASE_URL, version=3.4) + + def test_discover_individual_version_v2(self): + self.requests.get(V2_URL, text=V2_VERSION_ENTRY) + + self.assertCreatesV2(auth_url=V2_URL) + + def test_discover_individual_version_v3(self): + self.requests.get(V3_URL, text=V3_VERSION_ENTRY) + + self.assertCreatesV3(auth_url=V3_URL) + + def test_discover_individual_endpoint_v2(self): + self.requests.get(V2_URL, text=V2_VERSION_ENTRY) + self.assertCreatesV2(endpoint=V2_URL, token='fake-token') + + def test_discover_individual_endpoint_v3(self): + self.requests.get(V3_URL, text=V3_VERSION_ENTRY) + self.assertCreatesV3(endpoint=V3_URL, token='fake-token') + + def test_discover_fail_to_create_bad_individual_version(self): + self.requests.get(V2_URL, text=V2_VERSION_ENTRY) + self.requests.get(V3_URL, text=V3_VERSION_ENTRY) + + self.assertVersionNotAvailable(auth_url=V2_URL, version=3) + self.assertVersionNotAvailable(auth_url=V3_URL, version=2) + + def test_discover_unstable_versions(self): + version_list = fixture.DiscoveryList(BASE_URL, v3_status='beta') + self.requests.get(BASE_URL, status_code=300, json=version_list) + + self.assertCreatesV2(auth_url=BASE_URL) + self.assertVersionNotAvailable(auth_url=BASE_URL, version=3) + self.assertCreatesV3(auth_url=BASE_URL, unstable=True) + + def test_discover_forwards_original_ip(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + ip = '192.168.1.1' + self.assertCreatesV3(auth_url=BASE_URL, original_ip=ip) + + self.assertThat(self.requests.last_request.headers['forwarded'], + matchers.Contains(ip)) + + def test_discover_bad_args(self): + self.assertRaises(exceptions.DiscoveryFailure, + client.Client) + + def test_discover_bad_response(self): + self.requests.get(BASE_URL, status_code=300, json={'FOO': 'BAR'}) + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_discovery_ignore_invalid(self): + resp = [{'id': 'v3.0', + 'links': [1, 2, 3, 4], # invalid links + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED}] + self.requests.get(BASE_URL, status_code=300, + text=_create_version_list(resp)) + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_ignore_entry_without_links(self): + v3 = V3_VERSION.copy() + v3['links'] = [] + self.requests.get(BASE_URL, status_code=300, + text=_create_version_list([v3, V2_VERSION])) + self.assertCreatesV2(auth_url=BASE_URL) + + def test_ignore_entry_without_status(self): + v3 = V3_VERSION.copy() + del v3['status'] + self.requests.get(BASE_URL, status_code=300, + text=_create_version_list([v3, V2_VERSION])) + self.assertCreatesV2(auth_url=BASE_URL) + + def test_greater_version_than_required(self): + versions = fixture.DiscoveryList(BASE_URL, v3_id='v3.6') + self.requests.get(BASE_URL, json=versions) + self.assertCreatesV3(auth_url=BASE_URL, version=(3, 4)) + + def test_lesser_version_than_required(self): + versions = fixture.DiscoveryList(BASE_URL, v3_id='v3.4') + self.requests.get(BASE_URL, json=versions) + self.assertVersionNotAvailable(auth_url=BASE_URL, version=(3, 6)) + + def test_bad_response(self): + self.requests.get(BASE_URL, status_code=300, text="Ugly Duckling") + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_pass_client_arguments(self): + self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST) + kwargs = {'original_ip': '100', 'use_keyring': False, + 'stale_duration': 15} + + cl = self.assertCreatesV2(auth_url=BASE_URL, **kwargs) + + self.assertEqual(cl.original_ip, '100') + self.assertEqual(cl.stale_duration, 15) + self.assertFalse(cl.use_keyring) + + def test_overriding_stored_kwargs(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + self.requests.post("%s/auth/tokens" % V3_URL, + text=V3_AUTH_RESPONSE, + headers={'X-Subject-Token': V3_TOKEN}) + + disc = discover.Discover(auth_url=BASE_URL, debug=False, + username='foo') + client = disc.create_client(debug=True, password='bar') + + self.assertIsInstance(client, v3_client.Client) + self.assertTrue(client.debug_log) + self.assertFalse(disc._client_kwargs['debug']) + self.assertEqual(client.username, 'foo') + self.assertEqual(client.password, 'bar') + + def test_available_versions(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_ENTRY) + disc = discover.Discover(auth_url=BASE_URL) + + versions = disc.available_versions() + self.assertEqual(1, len(versions)) + self.assertEqual(V3_VERSION, versions[0]) + + def test_unknown_client_version(self): + V4_VERSION = {'id': 'v4.0', + 'links': [{'href': 'http://url', 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED} + versions = fixture.DiscoveryList() + versions.add_version(V4_VERSION) + self.requests.get(BASE_URL, status_code=300, json=versions) + + disc = discover.Discover(auth_url=BASE_URL) + self.assertRaises(exceptions.DiscoveryFailure, + disc.create_client, version=4) + + def test_discovery_fail_for_missing_v3(self): + versions = fixture.DiscoveryList(v2=True, v3=False) + self.requests.get(BASE_URL, status_code=300, json=versions) + + disc = discover.Discover(auth_url=BASE_URL) + self.assertRaises(exceptions.DiscoveryFailure, + disc.create_client, version=(3, 0)) + + def _do_discovery_call(self, token=None, **kwargs): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + if not token: + token = uuid.uuid4().hex + + url = 'http://testurl' + a = token_endpoint.Token(url, token) + s = session.Session(auth=a) + + # will default to true as there is a plugin on the session + discover.Discover(s, auth_url=BASE_URL, **kwargs) + + self.assertEqual(BASE_URL, self.requests.last_request.url) + + def test_setting_authenticated_true(self): + token = uuid.uuid4().hex + self._do_discovery_call(token) + self.assertRequestHeaderEqual('X-Auth-Token', token) + + def test_setting_authenticated_false(self): + self._do_discovery_call(authenticated=False) + self.assertNotIn('X-Auth-Token', self.requests.last_request.headers) + + +class DiscoverQueryTests(utils.TestCase): + + def test_available_keystone_data(self): + self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + disc = discover.Discover(auth_url=BASE_URL) + versions = disc.version_data() + + self.assertEqual((2, 0), versions[0]['version']) + self.assertEqual('stable', versions[0]['raw_status']) + self.assertEqual(V2_URL, versions[0]['url']) + self.assertEqual((3, 0), versions[1]['version']) + self.assertEqual('stable', versions[1]['raw_status']) + self.assertEqual(V3_URL, versions[1]['url']) + + version = disc.data_for('v3.0') + self.assertEqual((3, 0), version['version']) + self.assertEqual('stable', version['raw_status']) + self.assertEqual(V3_URL, version['url']) + + version = disc.data_for(2) + self.assertEqual((2, 0), version['version']) + self.assertEqual('stable', version['raw_status']) + self.assertEqual(V2_URL, version['url']) + + self.assertIsNone(disc.url_for('v4')) + self.assertEqual(V3_URL, disc.url_for('v3')) + self.assertEqual(V2_URL, disc.url_for('v2')) + + def test_available_cinder_data(self): + text = jsonutils.dumps(CINDER_EXAMPLES) + self.requests.get(BASE_URL, status_code=300, text=text) + + v1_url = "%sv1/" % BASE_URL + v2_url = "%sv2/" % BASE_URL + + disc = discover.Discover(auth_url=BASE_URL) + versions = disc.version_data() + + self.assertEqual((1, 0), versions[0]['version']) + self.assertEqual('CURRENT', versions[0]['raw_status']) + self.assertEqual(v1_url, versions[0]['url']) + self.assertEqual((2, 0), versions[1]['version']) + self.assertEqual('CURRENT', versions[1]['raw_status']) + self.assertEqual(v2_url, versions[1]['url']) + + version = disc.data_for('v2.0') + self.assertEqual((2, 0), version['version']) + self.assertEqual('CURRENT', version['raw_status']) + self.assertEqual(v2_url, version['url']) + + version = disc.data_for(1) + self.assertEqual((1, 0), version['version']) + self.assertEqual('CURRENT', version['raw_status']) + self.assertEqual(v1_url, version['url']) + + self.assertIsNone(disc.url_for('v3')) + self.assertEqual(v2_url, disc.url_for('v2')) + self.assertEqual(v1_url, disc.url_for('v1')) + + def test_available_glance_data(self): + text = jsonutils.dumps(GLANCE_EXAMPLES) + self.requests.get(BASE_URL, text=text) + + v1_url = "%sv1/" % BASE_URL + v2_url = "%sv2/" % BASE_URL + + disc = discover.Discover(auth_url=BASE_URL) + versions = disc.version_data() + + self.assertEqual((1, 0), versions[0]['version']) + self.assertEqual('SUPPORTED', versions[0]['raw_status']) + self.assertEqual(v1_url, versions[0]['url']) + self.assertEqual((1, 1), versions[1]['version']) + self.assertEqual('CURRENT', versions[1]['raw_status']) + self.assertEqual(v1_url, versions[1]['url']) + self.assertEqual((2, 0), versions[2]['version']) + self.assertEqual('SUPPORTED', versions[2]['raw_status']) + self.assertEqual(v2_url, versions[2]['url']) + self.assertEqual((2, 1), versions[3]['version']) + self.assertEqual('SUPPORTED', versions[3]['raw_status']) + self.assertEqual(v2_url, versions[3]['url']) + self.assertEqual((2, 2), versions[4]['version']) + self.assertEqual('CURRENT', versions[4]['raw_status']) + self.assertEqual(v2_url, versions[4]['url']) + + for ver in (2, 2.1, 2.2): + version = disc.data_for(ver) + self.assertEqual((2, 2), version['version']) + self.assertEqual('CURRENT', version['raw_status']) + self.assertEqual(v2_url, version['url']) + self.assertEqual(v2_url, disc.url_for(ver)) + + for ver in (1, 1.1): + version = disc.data_for(ver) + self.assertEqual((1, 1), version['version']) + self.assertEqual('CURRENT', version['raw_status']) + self.assertEqual(v1_url, version['url']) + self.assertEqual(v1_url, disc.url_for(ver)) + + self.assertIsNone(disc.url_for('v3')) + self.assertIsNone(disc.url_for('v2.3')) + + def test_allow_deprecated(self): + status = 'deprecated' + version_list = [{'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': status, + 'updated': UPDATED}] + text = jsonutils.dumps({'versions': version_list}) + self.requests.get(BASE_URL, text=text) + + disc = discover.Discover(auth_url=BASE_URL) + + # deprecated is allowed by default + versions = disc.version_data(allow_deprecated=False) + self.assertEqual(0, len(versions)) + + versions = disc.version_data(allow_deprecated=True) + self.assertEqual(1, len(versions)) + self.assertEqual(status, versions[0]['raw_status']) + self.assertEqual(V3_URL, versions[0]['url']) + self.assertEqual((3, 0), versions[0]['version']) + + def test_allow_experimental(self): + status = 'experimental' + version_list = [{'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': status, + 'updated': UPDATED}] + text = jsonutils.dumps({'versions': version_list}) + self.requests.get(BASE_URL, text=text) + + disc = discover.Discover(auth_url=BASE_URL) + + versions = disc.version_data() + self.assertEqual(0, len(versions)) + + versions = disc.version_data(allow_experimental=True) + self.assertEqual(1, len(versions)) + self.assertEqual(status, versions[0]['raw_status']) + self.assertEqual(V3_URL, versions[0]['url']) + self.assertEqual((3, 0), versions[0]['version']) + + def test_allow_unknown(self): + status = 'abcdef' + version_list = fixture.DiscoveryList(BASE_URL, v2=False, + v3_status=status) + self.requests.get(BASE_URL, json=version_list) + disc = discover.Discover(auth_url=BASE_URL) + + versions = disc.version_data() + self.assertEqual(0, len(versions)) + + versions = disc.version_data(allow_unknown=True) + self.assertEqual(1, len(versions)) + self.assertEqual(status, versions[0]['raw_status']) + self.assertEqual(V3_URL, versions[0]['url']) + self.assertEqual((3, 0), versions[0]['version']) + + def test_ignoring_invalid_lnks(self): + version_list = [{'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED}, + {'id': 'v3.1', + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED}, + {'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED, + 'links': [{'href': V3_URL, 'rel': 'self'}], + }] + + text = jsonutils.dumps({'versions': version_list}) + self.requests.get(BASE_URL, text=text) + + disc = discover.Discover(auth_url=BASE_URL) + + # raw_version_data will return all choices, even invalid ones + versions = disc.raw_version_data() + self.assertEqual(3, len(versions)) + + # only the version with both id and links will be actually returned + versions = disc.version_data() + self.assertEqual(1, len(versions)) + + +class CatalogHackTests(utils.TestCase): + + TEST_URL = 'http://keystone.server:5000/v2.0' + OTHER_URL = 'http://other.server:5000/path' + + IDENTITY = 'identity' + + BASE_URL = 'http://keystone.server:5000/' + V2_URL = BASE_URL + 'v2.0' + V3_URL = BASE_URL + 'v3' + + def setUp(self): + super(CatalogHackTests, self).setUp() + self.hacks = _discover._VersionHacks() + self.hacks.add_discover_hack(self.IDENTITY, + re.compile('/v2.0/?$'), + '/') + + def test_version_hacks(self): + self.assertEqual(self.BASE_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.V2_URL)) + + self.assertEqual(self.BASE_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.V2_URL + '/')) + + self.assertEqual(self.OTHER_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.OTHER_URL)) + + def test_ignored_non_service_type(self): + self.assertEqual(self.V2_URL, + self.hacks.get_discover_hack('other', self.V2_URL)) + + +class DiscoverUtils(utils.TestCase): + + def test_version_number(self): + def assertVersion(inp, out): + self.assertEqual(out, _discover.normalize_version_number(inp)) + + def versionRaises(inp): + self.assertRaises(TypeError, + _discover.normalize_version_number, + inp) + + assertVersion('v1.2', (1, 2)) + assertVersion('v11', (11, 0)) + assertVersion('1.2', (1, 2)) + assertVersion('1.5.1', (1, 5, 1)) + assertVersion('1', (1, 0)) + assertVersion(1, (1, 0)) + assertVersion(5.2, (5, 2)) + assertVersion((6, 1), (6, 1)) + assertVersion([1, 4], (1, 4)) + + versionRaises('hello') + versionRaises('1.a') + versionRaises('vacuum') diff --git a/keystoneclient/tests/unit/test_ec2utils.py b/keystoneclient/tests/unit/test_ec2utils.py new file mode 100644 index 0000000..71fc176 --- /dev/null +++ b/keystoneclient/tests/unit/test_ec2utils.py @@ -0,0 +1,262 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 __future__ import unicode_literals + +import testtools + +from keystoneclient.contrib.ec2 import utils + + +class Ec2SignerTest(testtools.TestCase): + + def setUp(self): + super(Ec2SignerTest, self).setUp() + self.access = '966afbde20b84200ae4e62e09acf46b2' + self.secret = '89cdf9e94e2643cab35b8b8ac5a51f83' + self.signer = utils.Ec2Signer(self.secret) + + def test_v4_creds_header(self): + auth_str = 'AWS4-HMAC-SHA256 blah' + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {}, + 'headers': {'Authorization': auth_str}} + self.assertTrue(self.signer._v4_creds(credentials)) + + def test_v4_creds_param(self): + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'X-Amz-Algorithm': 'AWS4-HMAC-SHA256'}, + 'headers': {}} + self.assertTrue(self.signer._v4_creds(credentials)) + + def test_v4_creds_false(self): + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '0', + 'AWSAccessKeyId': self.access, + 'Timestamp': '2012-11-27T11:47:02Z', + 'Action': 'Foo'}} + self.assertFalse(self.signer._v4_creds(credentials)) + + def test_generate_0(self): + """Test generate function for v0 signature.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '0', + 'AWSAccessKeyId': self.access, + 'Timestamp': '2012-11-27T11:47:02Z', + 'Action': 'Foo'}} + signature = self.signer.generate(credentials) + expected = 'SmXQEZAUdQw5glv5mX8mmixBtas=' + self.assertEqual(signature, expected) + + def test_generate_1(self): + """Test generate function for v1 signature.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '1', + 'AWSAccessKeyId': self.access}} + signature = self.signer.generate(credentials) + expected = 'VRnoQH/EhVTTLhwRLfuL7jmFW9c=' + self.assertEqual(signature, expected) + + def test_generate_v2_SHA256(self): + """Test generate function for v2 signature, SHA256.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '2', + 'AWSAccessKeyId': self.access}} + signature = self.signer.generate(credentials) + expected = 'odsGmT811GffUO0Eu13Pq+xTzKNIjJ6NhgZU74tYX/w=' + self.assertEqual(signature, expected) + + def test_generate_v2_SHA1(self): + """Test generate function for v2 signature, SHA1.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '2', + 'AWSAccessKeyId': self.access}} + self.signer.hmac_256 = None + signature = self.signer.generate(credentials) + expected = 'ZqCxMI4ZtTXWI175743mJ0hy/Gc=' + self.assertEqual(signature, expected) + + def test_generate_v4(self): + """Test v4 generator with data from AWS docs example. + + see: + http://docs.aws.amazon.com/general/latest/gr/ + sigv4-create-canonical-request.html + and + http://docs.aws.amazon.com/general/latest/gr/ + sigv4-signed-request-examples.html + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'iam.amazonaws.com', + 'Authorization': auth_str} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {'Action': 'CreateUser', + 'UserName': 'NewUser', + 'Version': '2010-05-08', + 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256', + 'X-Amz-Credential': 'AKIAEXAMPLE/20140611/' + 'us-east-1/iam/aws4_request', + 'X-Amz-Date': '20140611T231318Z', + 'X-Amz-Expires': '30', + 'X-Amz-SignedHeaders': 'host', + 'X-Amz-Signature': 'ced6826de92d2bdeed8f846f0bf508e8' + '559e98e4b0199114b84c54174deb456c'} + credentials = {'host': 'iam.amazonaws.com', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + expected = ('ced6826de92d2bdeed8f846f0bf508e8' + '559e98e4b0199114b84c54174deb456c') + self.assertEqual(signature, expected) + + def test_generate_v4_port(self): + """Test v4 generator with host:port format.""" + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(signature, expected) + + def test_generate_v4_port_strip(self): + """Test v4 generator with host:port format, but for an old + (<2.9.3) version of boto, where the port should be stripped + to match boto behavior. + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.9.2 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('9a4b2276a5039ada3b90f72ea8ec1745' + '14b92b909fb106b22ad910c5d75a54f4') + self.assertEqual(expected, signature) + + def test_generate_v4_port_nostrip(self): + """Test v4 generator with host:port format, but for an new + (>=2.9.3) version of boto, where the port should not be stripped. + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.9.3 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(expected, signature) diff --git a/keystoneclient/tests/unit/test_fixtures.py b/keystoneclient/tests/unit/test_fixtures.py new file mode 100644 index 0000000..8080c82 --- /dev/null +++ b/keystoneclient/tests/unit/test_fixtures.py @@ -0,0 +1,237 @@ +# 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 + +import six + +from keystoneclient import fixture +from keystoneclient.tests.unit import utils + + +class V2TokenTests(utils.TestCase): + + def test_unscoped(self): + token_id = uuid.uuid4().hex + user_id = uuid.uuid4().hex + user_name = uuid.uuid4().hex + + token = fixture.V2Token(token_id=token_id, + user_id=user_id, + user_name=user_name) + + self.assertEqual(token_id, token.token_id) + self.assertEqual(token_id, token['access']['token']['id']) + self.assertEqual(user_id, token.user_id) + self.assertEqual(user_id, token['access']['user']['id']) + self.assertEqual(user_name, token.user_name) + self.assertEqual(user_name, token['access']['user']['name']) + + def test_tenant_scoped(self): + tenant_id = uuid.uuid4().hex + tenant_name = uuid.uuid4().hex + + token = fixture.V2Token(tenant_id=tenant_id, + tenant_name=tenant_name) + + self.assertEqual(tenant_id, token.tenant_id) + self.assertEqual(tenant_id, token['access']['token']['tenant']['id']) + self.assertEqual(tenant_name, token.tenant_name) + tn = token['access']['token']['tenant']['name'] + self.assertEqual(tenant_name, tn) + + def test_trust_scoped(self): + trust_id = uuid.uuid4().hex + trustee_user_id = uuid.uuid4().hex + + token = fixture.V2Token(trust_id=trust_id, + trustee_user_id=trustee_user_id) + trust = token['access']['trust'] + + self.assertEqual(trust_id, token.trust_id) + self.assertEqual(trust_id, trust['id']) + self.assertEqual(trustee_user_id, token.trustee_user_id) + self.assertEqual(trustee_user_id, trust['trustee_user_id']) + + def test_roles(self): + role_id1 = uuid.uuid4().hex + role_name1 = uuid.uuid4().hex + role_id2 = uuid.uuid4().hex + role_name2 = uuid.uuid4().hex + + token = fixture.V2Token() + token.add_role(id=role_id1, name=role_name1) + token.add_role(id=role_id2, name=role_name2) + + role_names = token['access']['user']['roles'] + role_ids = token['access']['metadata']['roles'] + + self.assertEqual(set([role_id1, role_id2]), set(role_ids)) + for r in (role_name1, role_name2): + self.assertIn({'name': r}, role_names) + + def test_services(self): + service_type = uuid.uuid4().hex + service_name = uuid.uuid4().hex + region = uuid.uuid4().hex + + public = uuid.uuid4().hex + admin = uuid.uuid4().hex + internal = uuid.uuid4().hex + + token = fixture.V2Token() + svc = token.add_service(type=service_type, name=service_name) + + svc.add_endpoint(public=public, + admin=admin, + internal=internal, + region=region) + + self.assertEqual(1, len(token['access']['serviceCatalog'])) + service = token['access']['serviceCatalog'][0]['endpoints'][0] + + self.assertEqual(public, service['publicURL']) + self.assertEqual(internal, service['internalURL']) + self.assertEqual(admin, service['adminURL']) + self.assertEqual(region, service['region']) + + +class V3TokenTests(utils.TestCase): + + def test_unscoped(self): + user_id = uuid.uuid4().hex + user_name = uuid.uuid4().hex + user_domain_id = uuid.uuid4().hex + user_domain_name = uuid.uuid4().hex + + token = fixture.V3Token(user_id=user_id, + user_name=user_name, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name) + + self.assertEqual(user_id, token.user_id) + self.assertEqual(user_id, token['token']['user']['id']) + self.assertEqual(user_name, token.user_name) + self.assertEqual(user_name, token['token']['user']['name']) + + user_domain = token['token']['user']['domain'] + + self.assertEqual(user_domain_id, token.user_domain_id) + self.assertEqual(user_domain_id, user_domain['id']) + self.assertEqual(user_domain_name, token.user_domain_name) + self.assertEqual(user_domain_name, user_domain['name']) + + def test_project_scoped(self): + project_id = uuid.uuid4().hex + project_name = uuid.uuid4().hex + project_domain_id = uuid.uuid4().hex + project_domain_name = uuid.uuid4().hex + + token = fixture.V3Token(project_id=project_id, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name) + + self.assertEqual(project_id, token.project_id) + self.assertEqual(project_id, token['token']['project']['id']) + self.assertEqual(project_name, token.project_name) + self.assertEqual(project_name, token['token']['project']['name']) + + project_domain = token['token']['project']['domain'] + + self.assertEqual(project_domain_id, token.project_domain_id) + self.assertEqual(project_domain_id, project_domain['id']) + self.assertEqual(project_domain_name, token.project_domain_name) + self.assertEqual(project_domain_name, project_domain['name']) + + def test_domain_scoped(self): + domain_id = uuid.uuid4().hex + domain_name = uuid.uuid4().hex + + token = fixture.V3Token(domain_id=domain_id, + domain_name=domain_name) + + self.assertEqual(domain_id, token.domain_id) + self.assertEqual(domain_id, token['token']['domain']['id']) + self.assertEqual(domain_name, token.domain_name) + self.assertEqual(domain_name, token['token']['domain']['name']) + + def test_roles(self): + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + + token = fixture.V3Token() + token.add_role(**role1) + token.add_role(**role2) + + self.assertEqual(2, len(token['token']['roles'])) + + self.assertIn(role1, token['token']['roles']) + self.assertIn(role2, token['token']['roles']) + + def test_trust_scoped(self): + trust_id = uuid.uuid4().hex + trustee_user_id = uuid.uuid4().hex + trustor_user_id = uuid.uuid4().hex + impersonation = True + + token = fixture.V3Token(trust_id=trust_id, + trustee_user_id=trustee_user_id, + trustor_user_id=trustor_user_id, + trust_impersonation=impersonation) + + trust = token['token']['OS-TRUST:trust'] + self.assertEqual(trust_id, token.trust_id) + self.assertEqual(trust_id, trust['id']) + self.assertEqual(trustee_user_id, token.trustee_user_id) + self.assertEqual(trustee_user_id, trust['trustee_user']['id']) + self.assertEqual(trustor_user_id, token.trustor_user_id) + self.assertEqual(trustor_user_id, trust['trustor_user']['id']) + self.assertEqual(impersonation, token.trust_impersonation) + self.assertEqual(impersonation, trust['impersonation']) + + def test_oauth_scoped(self): + access_id = uuid.uuid4().hex + consumer_id = uuid.uuid4().hex + + token = fixture.V3Token(oauth_access_token_id=access_id, + oauth_consumer_id=consumer_id) + + oauth = token['token']['OS-OAUTH1'] + + self.assertEqual(access_id, token.oauth_access_token_id) + self.assertEqual(access_id, oauth['access_token_id']) + self.assertEqual(consumer_id, token.oauth_consumer_id) + self.assertEqual(consumer_id, oauth['consumer_id']) + + def test_catalog(self): + service_type = uuid.uuid4().hex + service_name = uuid.uuid4().hex + region = uuid.uuid4().hex + endpoints = {'public': uuid.uuid4().hex, + 'internal': uuid.uuid4().hex, + 'admin': uuid.uuid4().hex} + + token = fixture.V3Token() + svc = token.add_service(type=service_type, name=service_name) + svc.add_standard_endpoints(region=region, **endpoints) + + self.assertEqual(1, len(token['token']['catalog'])) + service = token['token']['catalog'][0] + self.assertEqual(3, len(service['endpoints'])) + + self.assertEqual(service_name, service['name']) + self.assertEqual(service_type, service['type']) + + for interface, url in six.iteritems(endpoints): + endpoint = {'interface': interface, 'url': url, 'region': region} + self.assertIn(endpoint, service['endpoints']) diff --git a/keystoneclient/tests/unit/test_hacking_checks.py b/keystoneclient/tests/unit/test_hacking_checks.py new file mode 100644 index 0000000..220d258 --- /dev/null +++ b/keystoneclient/tests/unit/test_hacking_checks.py @@ -0,0 +1,47 @@ +# 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 textwrap + +import mock +import pep8 +import testtools + +from keystoneclient.hacking import checks +from keystoneclient.tests.unit import client_fixtures + + +class TestCheckOsloNamespaceImports(testtools.TestCase): + + # We are patching pep8 so that only the check under test is actually + # installed. + @mock.patch('pep8._checks', + {'physical_line': {}, 'logical_line': {}, 'tree': {}}) + def run_check(self, code): + pep8.register_check(checks.check_oslo_namespace_imports) + + lines = textwrap.dedent(code).strip().splitlines(True) + + checker = pep8.Checker(lines=lines) + checker.check_all() + checker.report._deferred_print.sort() + return checker.report._deferred_print + + def assert_has_errors(self, code, expected_errors=None): + actual_errors = [e[:3] for e in self.run_check(code)] + self.assertEqual(expected_errors or [], actual_errors) + + def test(self): + code_ex = self.useFixture(client_fixtures.HackingCode()) + code = code_ex.oslo_namespace_imports['code'] + errors = code_ex.oslo_namespace_imports['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) diff --git a/keystoneclient/tests/unit/test_http.py b/keystoneclient/tests/unit/test_http.py new file mode 100644 index 0000000..6dfceec --- /dev/null +++ b/keystoneclient/tests/unit/test_http.py @@ -0,0 +1,208 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 + +import six +from testtools import matchers + +from keystoneclient import exceptions +from keystoneclient import httpclient +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +RESPONSE_BODY = '{"hi": "there"}' + + +def get_client(): + cl = httpclient.HTTPClient(username="username", password="password", + tenant_id="tenant", auth_url="auth_test") + return cl + + +def get_authed_client(): + cl = get_client() + cl.management_url = "http://127.0.0.1:5000" + cl.auth_token = "token" + return cl + + +class FakeLog(object): + def __init__(self): + self.warn_log = str() + self.debug_log = str() + + def warn(self, msg=None, *args, **kwargs): + self.warn_log = "%s\n%s" % (self.warn_log, (msg % args)) + + def debug(self, msg=None, *args, **kwargs): + self.debug_log = "%s\n%s" % (self.debug_log, (msg % args)) + + +class ClientTest(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/hi' + + def test_unauthorized_client_requests(self): + cl = get_client() + self.assertRaises(exceptions.AuthorizationFailure, cl.get, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.post, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.put, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.delete, '/hi') + + def test_get(self): + cl = get_authed_client() + + self.stub_url('GET', text=RESPONSE_BODY) + + resp, body = cl.get("/hi") + self.assertEqual(self.requests.last_request.method, 'GET') + self.assertEqual(self.requests.last_request.url, self.TEST_URL) + + self.assertRequestHeaderEqual('X-Auth-Token', 'token') + self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT) + + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + def test_get_error_with_plaintext_resp(self): + cl = get_authed_client() + self.stub_url('GET', status_code=400, + text='Some evil plaintext string') + + self.assertRaises(exceptions.BadRequest, cl.get, '/hi') + + def test_get_error_with_json_resp(self): + cl = get_authed_client() + err_response = { + "error": { + "code": 400, + "title": "Error title", + "message": "Error message string" + } + } + self.stub_url('GET', status_code=400, json=err_response) + exc_raised = False + try: + cl.get('/hi') + except exceptions.BadRequest as exc: + exc_raised = True + self.assertEqual(exc.message, "Error message string") + self.assertTrue(exc_raised, 'Exception not raised.') + + def test_post(self): + cl = get_authed_client() + + self.stub_url('POST') + cl.post("/hi", body=[1, 2, 3]) + + self.assertEqual(self.requests.last_request.method, 'POST') + self.assertEqual(self.requests.last_request.body, '[1, 2, 3]') + + self.assertRequestHeaderEqual('X-Auth-Token', 'token') + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT) + + def test_forwarded_for(self): + ORIGINAL_IP = "10.100.100.1" + cl = httpclient.HTTPClient(username="username", password="password", + tenant_id="tenant", auth_url="auth_test", + original_ip=ORIGINAL_IP) + + self.stub_url('GET') + + cl.request(self.TEST_URL, 'GET') + forwarded = "for=%s;by=%s" % (ORIGINAL_IP, httpclient.USER_AGENT) + self.assertRequestHeaderEqual('Forwarded', forwarded) + + def test_client_deprecated(self): + # Can resolve symbols from the keystoneclient.client module. + # keystoneclient.client was deprecated and renamed to + # keystoneclient.httpclient. This tests that keystoneclient.client + # can still be used. + + from keystoneclient import client + + # These statements will raise an AttributeError if the symbol isn't + # defined in the module. + + client.HTTPClient + + +class BasicRequestTests(utils.TestCase): + + url = 'http://keystone.test.com/' + + def setUp(self): + super(BasicRequestTests, self).setUp() + self.logger_message = six.moves.cStringIO() + handler = logging.StreamHandler(self.logger_message) + handler.setLevel(logging.DEBUG) + + self.logger = logging.getLogger(session.__name__) + level = self.logger.getEffectiveLevel() + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(handler) + + self.addCleanup(self.logger.removeHandler, handler) + self.addCleanup(self.logger.setLevel, level) + + def request(self, method='GET', response='Test Response', status_code=200, + url=None, **kwargs): + if not url: + url = self.url + + self.requests.register_uri(method, url, text=response, + status_code=status_code) + + return httpclient.request(url, method, **kwargs) + + def test_basic_params(self): + method = 'GET' + response = 'Test Response' + status = 200 + + self.request(method=method, status_code=status, response=response) + + self.assertEqual(self.requests.last_request.method, method) + + logger_message = self.logger_message.getvalue() + + self.assertThat(logger_message, matchers.Contains('curl')) + self.assertThat(logger_message, matchers.Contains('-X %s' % + method)) + self.assertThat(logger_message, matchers.Contains(self.url)) + + self.assertThat(logger_message, matchers.Contains(str(status))) + self.assertThat(logger_message, matchers.Contains(response)) + + def test_headers(self): + headers = {'key': 'val', 'test': 'other'} + + self.request(headers=headers) + + for k, v in six.iteritems(headers): + self.assertRequestHeaderEqual(k, v) + + for header in six.iteritems(headers): + self.assertThat(self.logger_message.getvalue(), + matchers.Contains('-H "%s: %s"' % header)) + + def test_body(self): + data = "BODY DATA" + self.request(response=data) + logger_message = self.logger_message.getvalue() + self.assertThat(logger_message, matchers.Contains('BODY:')) + self.assertThat(logger_message, matchers.Contains(data)) diff --git a/keystoneclient/tests/unit/test_https.py b/keystoneclient/tests/unit/test_https.py new file mode 100644 index 0000000..e574d37 --- /dev/null +++ b/keystoneclient/tests/unit/test_https.py @@ -0,0 +1,107 @@ +# 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 +import requests + +from keystoneclient import httpclient +from keystoneclient.tests.unit import utils + + +FAKE_RESPONSE = utils.TestResponse({ + "status_code": 200, + "text": '{"hi": "there"}', +}) + +REQUEST_URL = 'https://127.0.0.1:5000/hi' +RESPONSE_BODY = '{"hi": "there"}' + + +def get_client(): + cl = httpclient.HTTPClient(username="username", password="password", + tenant_id="tenant", auth_url="auth_test", + cacert="ca.pem", key="key.pem", cert="cert.pem") + return cl + + +def get_authed_client(): + cl = get_client() + cl.management_url = "https://127.0.0.1:5000" + cl.auth_token = "token" + return cl + + +class ClientTest(utils.TestCase): + + def setUp(self): + super(ClientTest, self).setUp() + self.request_patcher = mock.patch.object(requests, 'request', + self.mox.CreateMockAnything()) + self.request_patcher.start() + self.addCleanup(self.request_patcher.stop) + + @mock.patch.object(requests, 'request') + def test_get(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE + cl = get_authed_client() + + resp, body = cl.get("/hi") + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'GET') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') + + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + @mock.patch.object(requests, 'request') + def test_post(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE + cl = get_authed_client() + + cl.post("/hi", body=[1, 2, 3]) + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') + + @mock.patch.object(requests, 'request') + def test_post_auth(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE + cl = httpclient.HTTPClient( + username="username", password="password", tenant_id="tenant", + auth_url="auth_test", cacert="ca.pem", key="key.pem", + cert="cert.pem") + cl.management_url = "https://127.0.0.1:5000" + cl.auth_token = "token" + cl.post("/hi", body=[1, 2, 3]) + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') diff --git a/keystoneclient/tests/unit/test_keyring.py b/keystoneclient/tests/unit/test_keyring.py new file mode 100644 index 0000000..a54009e --- /dev/null +++ b/keystoneclient/tests/unit/test_keyring.py @@ -0,0 +1,187 @@ +# 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 datetime + +import mock +from oslo_utils import timeutils + +from keystoneclient import access +from keystoneclient import httpclient +from keystoneclient.tests.unit import utils +from keystoneclient.tests.unit.v2_0 import client_fixtures + +try: + import keyring # noqa + import pickle # noqa +except ImportError: + keyring = None + + +PROJECT_SCOPED_TOKEN = client_fixtures.project_scoped_token() + +# These mirror values from PROJECT_SCOPED_TOKEN +USERNAME = 'exampleuser' +AUTH_URL = 'http://public.com:5000/v2.0' +TOKEN = '04c7d5ffaeef485f9dc69c06db285bdb' + +PASSWORD = 'password' +TENANT = 'tenant' +TENANT_ID = 'tenant_id' + + +class KeyringTest(utils.TestCase): + + def setUp(self): + if keyring is None: + self.skipTest( + 'optional package keyring or pickle is not installed') + + class MemoryKeyring(keyring.backend.KeyringBackend): + """A Simple testing keyring. + + This class supports stubbing an initial password to be returned by + setting password, and allows easy password and key retrieval. Also + records if a password was retrieved. + """ + def __init__(self): + self.key = None + self.password = None + self.fetched = False + self.get_password_called = False + self.set_password_called = False + + def supported(self): + return 1 + + def get_password(self, service, username): + self.get_password_called = True + key = username + '@' + service + # make sure we don't get passwords crossed if one is enforced. + if self.key and self.key != key: + return None + if self.password: + self.fetched = True + return self.password + + def set_password(self, service, username, password): + self.set_password_called = True + self.key = username + '@' + service + self.password = password + + super(KeyringTest, self).setUp() + self.memory_keyring = MemoryKeyring() + keyring.set_keyring(self.memory_keyring) + + def test_no_keyring_key(self): + """Ensure that if we don't have use_keyring set in the client that + the keyring is never accessed. + """ + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL) + + # stub and check that a new token is received + method = 'get_raw_token_from_identity_service' + with mock.patch.object(cl, method) as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + self.assertEqual(1, meth.call_count) + + # make sure that we never touched the keyring + self.assertFalse(self.memory_keyring.get_password_called) + self.assertFalse(self.memory_keyring.set_password_called) + + def test_build_keyring_key(self): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL) + + keyring_key = cl._build_keyring_key(auth_url=AUTH_URL, + username=USERNAME, + tenant_name=TENANT, + tenant_id=TENANT_ID, + token=TOKEN) + + self.assertEqual(keyring_key, + '%s/%s/%s/%s/%s' % + (AUTH_URL, TENANT_ID, TENANT, TOKEN, USERNAME)) + + def test_set_and_get_keyring_expired(self): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # set an expired token into the keyring + auth_ref = access.AccessInfo.factory(body=PROJECT_SCOPED_TOKEN) + expired = timeutils.utcnow() - datetime.timedelta(minutes=30) + auth_ref['token']['expires'] = timeutils.isotime(expired) + self.memory_keyring.password = pickle.dumps(auth_ref) + + # stub and check that a new token is received, so not using expired + method = 'get_raw_token_from_identity_service' + with mock.patch.object(cl, method) as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + self.assertEqual(1, meth.call_count) + + # check that a value was returned from the keyring + self.assertTrue(self.memory_keyring.fetched) + + # check that the new token has been loaded into the keyring + new_auth_ref = pickle.loads(self.memory_keyring.password) + self.assertEqual(new_auth_ref['token']['expires'], + PROJECT_SCOPED_TOKEN['access']['token']['expires']) + + def test_get_keyring(self): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # set an token into the keyring + auth_ref = access.AccessInfo.factory(body=PROJECT_SCOPED_TOKEN) + future = timeutils.utcnow() + datetime.timedelta(minutes=30) + auth_ref['token']['expires'] = timeutils.isotime(future) + self.memory_keyring.password = pickle.dumps(auth_ref) + + # don't stub get_raw_token so will fail if authenticate happens + + self.assertTrue(cl.authenticate()) + self.assertTrue(self.memory_keyring.fetched) + + def test_set_keyring(self): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # stub and check that a new token is received + method = 'get_raw_token_from_identity_service' + with mock.patch.object(cl, method) as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + self.assertEqual(1, meth.call_count) + + # we checked the keyring, but we didn't find anything + self.assertTrue(self.memory_keyring.get_password_called) + self.assertFalse(self.memory_keyring.fetched) + + # check that the new token has been loaded into the keyring + self.assertTrue(self.memory_keyring.set_password_called) + new_auth_ref = pickle.loads(self.memory_keyring.password) + self.assertEqual(new_auth_ref.auth_token, TOKEN) + self.assertEqual(new_auth_ref['token'], + PROJECT_SCOPED_TOKEN['access']['token']) + self.assertEqual(new_auth_ref.username, USERNAME) diff --git a/keystoneclient/tests/unit/test_memcache_crypt.py b/keystoneclient/tests/unit/test_memcache_crypt.py new file mode 100644 index 0000000..be07b24 --- /dev/null +++ b/keystoneclient/tests/unit/test_memcache_crypt.py @@ -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 six +import testtools + +from keystoneclient.middleware import memcache_crypt + + +class MemcacheCryptPositiveTests(testtools.TestCase): + def _setup_keys(self, strategy): + return memcache_crypt.derive_keys(b'token', b'secret', strategy) + + def test_constant_time_compare(self): + # make sure it works as a compare, the "constant time" aspect + # isn't appropriate to test in unittests + ctc = memcache_crypt.constant_time_compare + self.assertTrue(ctc('abcd', 'abcd')) + self.assertTrue(ctc('', '')) + self.assertFalse(ctc('abcd', 'efgh')) + self.assertFalse(ctc('abc', 'abcd')) + self.assertFalse(ctc('abc', 'abc\x00')) + self.assertFalse(ctc('', 'abc')) + + # For Python 3, we want to test these functions with both str and bytes + # as input. + if six.PY3: + self.assertTrue(ctc(b'abcd', b'abcd')) + self.assertTrue(ctc(b'', b'')) + self.assertFalse(ctc(b'abcd', b'efgh')) + self.assertFalse(ctc(b'abc', b'abcd')) + self.assertFalse(ctc(b'abc', b'abc\x00')) + self.assertFalse(ctc(b'', b'abc')) + + def test_derive_keys(self): + keys = self._setup_keys(b'strategy') + self.assertEqual(len(keys['ENCRYPTION']), + len(keys['CACHE_KEY'])) + self.assertEqual(len(keys['CACHE_KEY']), + len(keys['MAC'])) + self.assertNotEqual(keys['ENCRYPTION'], + keys['MAC']) + self.assertIn('strategy', keys.keys()) + + def test_key_strategy_diff(self): + k1 = self._setup_keys(b'MAC') + k2 = self._setup_keys(b'ENCRYPT') + self.assertNotEqual(k1, k2) + + def test_sign_data(self): + keys = self._setup_keys(b'MAC') + sig = memcache_crypt.sign_data(keys['MAC'], b'data') + self.assertEqual(len(sig), memcache_crypt.DIGEST_LENGTH_B64) + + def test_encryption(self): + keys = self._setup_keys(b'ENCRYPT') + # what you put in is what you get out + for data in [b'data', b'1234567890123456', b'\x00\xFF' * 13 + ] + [six.int2byte(x % 256) * x for x in range(768)]: + crypt = memcache_crypt.encrypt_data(keys['ENCRYPTION'], data) + decrypt = memcache_crypt.decrypt_data(keys['ENCRYPTION'], crypt) + self.assertEqual(data, decrypt) + self.assertRaises(memcache_crypt.DecryptError, + memcache_crypt.decrypt_data, + keys['ENCRYPTION'], crypt[:-1]) + + def test_protect_wrappers(self): + data = b'My Pretty Little Data' + for strategy in [b'MAC', b'ENCRYPT']: + keys = self._setup_keys(strategy) + protected = memcache_crypt.protect_data(keys, data) + self.assertNotEqual(protected, data) + if strategy == b'ENCRYPT': + self.assertNotIn(data, protected) + unprotected = memcache_crypt.unprotect_data(keys, protected) + self.assertEqual(data, unprotected) + self.assertRaises(memcache_crypt.InvalidMacError, + memcache_crypt.unprotect_data, + keys, protected[:-1]) + self.assertIsNone(memcache_crypt.unprotect_data(keys, None)) + + def test_no_pycrypt(self): + aes = memcache_crypt.AES + memcache_crypt.AES = None + self.assertRaises(memcache_crypt.CryptoUnavailableError, + memcache_crypt.encrypt_data, 'token', 'secret', + 'data') + memcache_crypt.AES = aes diff --git a/keystoneclient/tests/unit/test_s3_token_middleware.py b/keystoneclient/tests/unit/test_s3_token_middleware.py new file mode 100644 index 0000000..63f9e72 --- /dev/null +++ b/keystoneclient/tests/unit/test_s3_token_middleware.py @@ -0,0 +1,233 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_serialization import jsonutils +import requests +import six +import testtools +import webob + +from keystoneclient.middleware import s3_token +from keystoneclient.tests.unit import utils + + +GOOD_RESPONSE = {'access': {'token': {'id': 'TOKEN_ID', + 'tenant': {'id': 'TENANT_ID'}}}} + + +class FakeApp(object): + """This represents a WSGI app protected by the auth_token middleware.""" + def __call__(self, env, start_response): + resp = webob.Response() + resp.environ = env + return resp(env, start_response) + + +class S3TokenMiddlewareTestBase(utils.TestCase): + + TEST_PROTOCOL = 'https' + TEST_HOST = 'fakehost' + TEST_PORT = 35357 + TEST_URL = '%s://%s:%d/v2.0/s3tokens' % (TEST_PROTOCOL, + TEST_HOST, + TEST_PORT) + + def setUp(self): + super(S3TokenMiddlewareTestBase, self).setUp() + + self.conf = { + 'auth_host': self.TEST_HOST, + 'auth_port': self.TEST_PORT, + 'auth_protocol': self.TEST_PROTOCOL, + } + + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + + +class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase): + + def setUp(self): + super(S3TokenMiddlewareTestGood, self).setUp() + self.middleware = s3_token.S3Token(FakeApp(), self.conf) + + self.requests.post(self.TEST_URL, status_code=201, json=GOOD_RESPONSE) + + # Ignore the request and pass to the next middleware in the + # pipeline if no path has been specified. + def test_no_path_request(self): + req = webob.Request.blank('/') + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + + # Ignore the request and pass to the next middleware in the + # pipeline if no Authorization header has been specified + def test_without_authorization(self): + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + + def test_without_auth_storage_token(self): + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'badboy' + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + + def test_authorized(self): + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'access:signature' + req.headers['X-Storage-Token'] = 'token' + req.get_response(self.middleware) + self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID')) + self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID') + + def test_authorized_http(self): + TEST_URL = 'http://%s:%d/v2.0/s3tokens' % (self.TEST_HOST, + self.TEST_PORT) + + self.requests.post(TEST_URL, status_code=201, json=GOOD_RESPONSE) + + self.middleware = ( + s3_token.filter_factory({'auth_protocol': 'http', + 'auth_host': self.TEST_HOST, + 'auth_port': self.TEST_PORT})(FakeApp())) + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'access:signature' + req.headers['X-Storage-Token'] = 'token' + req.get_response(self.middleware) + self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID')) + self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID') + + def test_authorization_nova_toconnect(self): + req = webob.Request.blank('/v1/AUTH_swiftint/c/o') + req.headers['Authorization'] = 'access:FORCED_TENANT_ID:signature' + req.headers['X-Storage-Token'] = 'token' + req.get_response(self.middleware) + path = req.environ['PATH_INFO'] + self.assertTrue(path.startswith('/v1/AUTH_FORCED_TENANT_ID')) + + @mock.patch.object(requests, 'post') + def test_insecure(self, MOCK_REQUEST): + self.middleware = ( + s3_token.filter_factory({'insecure': True})(FakeApp())) + + text_return_value = jsonutils.dumps(GOOD_RESPONSE) + if six.PY3: + text_return_value = text_return_value.encode() + MOCK_REQUEST.return_value = utils.TestResponse({ + 'status_code': 201, + 'text': text_return_value}) + + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'access:signature' + req.headers['X-Storage-Token'] = 'token' + req.get_response(self.middleware) + + self.assertTrue(MOCK_REQUEST.called) + mock_args, mock_kwargs = MOCK_REQUEST.call_args + self.assertIs(mock_kwargs['verify'], False) + + +class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase): + def setUp(self): + super(S3TokenMiddlewareTestBad, self).setUp() + self.middleware = s3_token.S3Token(FakeApp(), self.conf) + + def test_unauthorized_token(self): + ret = {"error": + {"message": "EC2 access key not found.", + "code": 401, + "title": "Unauthorized"}} + self.requests.post(self.TEST_URL, status_code=403, json=ret) + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'access:signature' + req.headers['X-Storage-Token'] = 'token' + resp = req.get_response(self.middleware) + s3_denied_req = self.middleware.deny_request('AccessDenied') + self.assertEqual(resp.body, s3_denied_req.body) + self.assertEqual(resp.status_int, s3_denied_req.status_int) + + def test_bogus_authorization(self): + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'badboy' + req.headers['X-Storage-Token'] = 'token' + resp = req.get_response(self.middleware) + self.assertEqual(resp.status_int, 400) + s3_invalid_req = self.middleware.deny_request('InvalidURI') + self.assertEqual(resp.body, s3_invalid_req.body) + self.assertEqual(resp.status_int, s3_invalid_req.status_int) + + def test_fail_to_connect_to_keystone(self): + with mock.patch.object(self.middleware, '_json_request') as o: + s3_invalid_req = self.middleware.deny_request('InvalidURI') + o.side_effect = s3_token.ServiceError(s3_invalid_req) + + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'access:signature' + req.headers['X-Storage-Token'] = 'token' + resp = req.get_response(self.middleware) + self.assertEqual(resp.body, s3_invalid_req.body) + self.assertEqual(resp.status_int, s3_invalid_req.status_int) + + def test_bad_reply(self): + self.requests.post(self.TEST_URL, status_code=201, text="<badreply>") + + req = webob.Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'access:signature' + req.headers['X-Storage-Token'] = 'token' + resp = req.get_response(self.middleware) + s3_invalid_req = self.middleware.deny_request('InvalidURI') + self.assertEqual(resp.body, s3_invalid_req.body) + self.assertEqual(resp.status_int, s3_invalid_req.status_int) + + +class S3TokenMiddlewareTestUtil(testtools.TestCase): + def test_split_path_failed(self): + self.assertRaises(ValueError, s3_token.split_path, '') + self.assertRaises(ValueError, s3_token.split_path, '/') + self.assertRaises(ValueError, s3_token.split_path, '//') + self.assertRaises(ValueError, s3_token.split_path, '//a') + self.assertRaises(ValueError, s3_token.split_path, '/a/c') + self.assertRaises(ValueError, s3_token.split_path, '//c') + self.assertRaises(ValueError, s3_token.split_path, '/a/c/') + self.assertRaises(ValueError, s3_token.split_path, '/a//') + self.assertRaises(ValueError, s3_token.split_path, '/a', 2) + self.assertRaises(ValueError, s3_token.split_path, '/a', 2, 3) + self.assertRaises(ValueError, s3_token.split_path, '/a', 2, 3, True) + self.assertRaises(ValueError, s3_token.split_path, '/a/c/o/r', 3, 3) + self.assertRaises(ValueError, s3_token.split_path, '/a', 5, 4) + + def test_split_path_success(self): + self.assertEqual(s3_token.split_path('/a'), ['a']) + self.assertEqual(s3_token.split_path('/a/'), ['a']) + self.assertEqual(s3_token.split_path('/a/c', 2), ['a', 'c']) + self.assertEqual(s3_token.split_path('/a/c/o', 3), ['a', 'c', 'o']) + self.assertEqual(s3_token.split_path('/a/c/o/r', 3, 3, True), + ['a', 'c', 'o/r']) + self.assertEqual(s3_token.split_path('/a/c', 2, 3, True), + ['a', 'c', None]) + self.assertEqual(s3_token.split_path('/a/c/', 2), ['a', 'c']) + self.assertEqual(s3_token.split_path('/a/c/', 2, 3), ['a', 'c', '']) + + def test_split_path_invalid_path(self): + try: + s3_token.split_path('o\nn e', 2) + except ValueError as err: + self.assertEqual(str(err), 'Invalid path: o%0An%20e') + try: + s3_token.split_path('o\nn e', 2, 3, True) + except ValueError as err: + self.assertEqual(str(err), 'Invalid path: o%0An%20e') diff --git a/keystoneclient/tests/unit/test_session.py b/keystoneclient/tests/unit/test_session.py new file mode 100644 index 0000000..1d01c3a --- /dev/null +++ b/keystoneclient/tests/unit/test_session.py @@ -0,0 +1,866 @@ +# 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 argparse +import itertools +import uuid + +import mock +from oslo_config import cfg +from oslo_config import fixture as config +from oslo_serialization import jsonutils +import requests +import six +from testtools import matchers + +from keystoneclient import adapter +from keystoneclient.auth import base +from keystoneclient import exceptions +from keystoneclient import session as client_session +from keystoneclient.tests.unit import utils + + +class SessionTests(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/' + + def test_get(self): + session = client_session.Session() + self.stub_url('GET', text='response') + resp = session.get(self.TEST_URL) + + self.assertEqual('GET', self.requests.last_request.method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + + def test_post(self): + session = client_session.Session() + self.stub_url('POST', text='response') + resp = session.post(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual('POST', self.requests.last_request.method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + self.assertRequestBodyIs(json={'hello': 'world'}) + + def test_head(self): + session = client_session.Session() + self.stub_url('HEAD') + resp = session.head(self.TEST_URL) + + self.assertEqual('HEAD', self.requests.last_request.method) + self.assertTrue(resp.ok) + self.assertRequestBodyIs('') + + def test_put(self): + session = client_session.Session() + self.stub_url('PUT', text='response') + resp = session.put(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual('PUT', self.requests.last_request.method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + self.assertRequestBodyIs(json={'hello': 'world'}) + + def test_delete(self): + session = client_session.Session() + self.stub_url('DELETE', text='response') + resp = session.delete(self.TEST_URL) + + self.assertEqual('DELETE', self.requests.last_request.method) + self.assertTrue(resp.ok) + self.assertEqual(resp.text, 'response') + + def test_patch(self): + session = client_session.Session() + self.stub_url('PATCH', text='response') + resp = session.patch(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual('PATCH', self.requests.last_request.method) + self.assertTrue(resp.ok) + self.assertEqual(resp.text, 'response') + self.assertRequestBodyIs(json={'hello': 'world'}) + + def test_user_agent(self): + session = client_session.Session(user_agent='test-agent') + self.stub_url('GET', text='response') + resp = session.get(self.TEST_URL) + + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'test-agent') + + resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}) + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'new-agent') + + resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}, + user_agent='overrides-agent') + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'overrides-agent') + + def test_http_session_opts(self): + session = client_session.Session(cert='cert.pem', timeout=5, + verify='certs') + + FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'}) + RESP = mock.Mock(return_value=FAKE_RESP) + + with mock.patch.object(session.session, 'request', RESP) as mocked: + session.post(self.TEST_URL, data='value') + + mock_args, mock_kwargs = mocked.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], self.TEST_URL) + self.assertEqual(mock_kwargs['data'], 'value') + self.assertEqual(mock_kwargs['cert'], 'cert.pem') + self.assertEqual(mock_kwargs['verify'], 'certs') + self.assertEqual(mock_kwargs['timeout'], 5) + + def test_not_found(self): + session = client_session.Session() + self.stub_url('GET', status_code=404) + self.assertRaises(exceptions.NotFound, session.get, self.TEST_URL) + + def test_server_error(self): + session = client_session.Session() + self.stub_url('GET', status_code=500) + self.assertRaises(exceptions.InternalServerError, + session.get, self.TEST_URL) + + def test_session_debug_output(self): + """Test request and response headers in debug logs + + in order to redact secure headers while debug is true. + """ + session = client_session.Session(verify=False) + headers = {'HEADERA': 'HEADERVALB'} + security_headers = {'Authorization': uuid.uuid4().hex, + 'X-Auth-Token': uuid.uuid4().hex, + 'X-Subject-Token': uuid.uuid4().hex, } + body = 'BODYRESPONSE' + data = 'BODYDATA' + all_headers = dict( + itertools.chain(headers.items(), security_headers.items())) + self.stub_url('POST', text=body, headers=all_headers) + resp = session.post(self.TEST_URL, headers=all_headers, data=data) + self.assertEqual(resp.status_code, 200) + + self.assertIn('curl', self.logger.output) + self.assertIn('POST', self.logger.output) + self.assertIn('--insecure', self.logger.output) + self.assertIn(body, self.logger.output) + self.assertIn("'%s'" % data, self.logger.output) + + for k, v in six.iteritems(headers): + self.assertIn(k, self.logger.output) + self.assertIn(v, self.logger.output) + + # Assert that response headers contains actual values and + # only debug logs has been masked + for k, v in six.iteritems(security_headers): + self.assertIn('%s: {SHA1}' % k, self.logger.output) + self.assertEqual(v, resp.headers[k]) + self.assertNotIn(v, self.logger.output) + + def test_logging_cacerts(self): + path_to_certs = '/path/to/certs' + session = client_session.Session(verify=path_to_certs) + + self.stub_url('GET', text='text') + session.get(self.TEST_URL) + + self.assertIn('--cacert', self.logger.output) + self.assertIn(path_to_certs, self.logger.output) + + def test_connect_retries(self): + + def _timeout_error(request, context): + raise requests.exceptions.Timeout() + + self.stub_url('GET', text=_timeout_error) + + session = client_session.Session() + retries = 3 + + with mock.patch('time.sleep') as m: + self.assertRaises(exceptions.RequestTimeout, + session.get, + self.TEST_URL, connect_retries=retries) + + self.assertEqual(retries, m.call_count) + # 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0 + m.assert_called_with(2.0) + + # we count retries so there will be one initial request + 3 retries + self.assertThat(self.requests.request_history, + matchers.HasLength(retries + 1)) + + def test_uses_tcp_keepalive_by_default(self): + session = client_session.Session() + requests_session = session.session + self.assertIsInstance(requests_session.adapters['http://'], + client_session.TCPKeepAliveAdapter) + self.assertIsInstance(requests_session.adapters['https://'], + client_session.TCPKeepAliveAdapter) + + def test_does_not_set_tcp_keepalive_on_custom_sessions(self): + mock_session = mock.Mock() + client_session.Session(session=mock_session) + self.assertFalse(mock_session.mount.called) + + +class RedirectTests(utils.TestCase): + + REDIRECT_CHAIN = ['http://myhost:3445/', + 'http://anotherhost:6555/', + 'http://thirdhost/', + 'http://finaldestination:55/'] + + DEFAULT_REDIRECT_BODY = 'Redirect' + DEFAULT_RESP_BODY = 'Found' + + def setup_redirects(self, method='GET', status_code=305, + redirect_kwargs={}, final_kwargs={}): + redirect_kwargs.setdefault('text', self.DEFAULT_REDIRECT_BODY) + + for s, d in zip(self.REDIRECT_CHAIN, self.REDIRECT_CHAIN[1:]): + self.requests.register_uri(method, s, status_code=status_code, + headers={'Location': d}, + **redirect_kwargs) + + final_kwargs.setdefault('status_code', 200) + final_kwargs.setdefault('text', self.DEFAULT_RESP_BODY) + self.requests.register_uri(method, self.REDIRECT_CHAIN[-1], + **final_kwargs) + + def assertResponse(self, resp): + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.text, self.DEFAULT_RESP_BODY) + + def test_basic_get(self): + session = client_session.Session() + self.setup_redirects() + resp = session.get(self.REDIRECT_CHAIN[-2]) + self.assertResponse(resp) + + def test_basic_post_keeps_correct_method(self): + session = client_session.Session() + self.setup_redirects(method='POST', status_code=301) + resp = session.post(self.REDIRECT_CHAIN[-2]) + self.assertResponse(resp) + + def test_redirect_forever(self): + session = client_session.Session(redirect=True) + self.setup_redirects() + resp = session.get(self.REDIRECT_CHAIN[0]) + self.assertResponse(resp) + self.assertTrue(len(resp.history), len(self.REDIRECT_CHAIN)) + + def test_no_redirect(self): + session = client_session.Session(redirect=False) + self.setup_redirects() + resp = session.get(self.REDIRECT_CHAIN[0]) + self.assertEqual(resp.status_code, 305) + self.assertEqual(resp.url, self.REDIRECT_CHAIN[0]) + + def test_redirect_limit(self): + self.setup_redirects() + for i in (1, 2): + session = client_session.Session(redirect=i) + resp = session.get(self.REDIRECT_CHAIN[0]) + self.assertEqual(resp.status_code, 305) + self.assertEqual(resp.url, self.REDIRECT_CHAIN[i]) + self.assertEqual(resp.text, self.DEFAULT_REDIRECT_BODY) + + def test_history_matches_requests(self): + self.setup_redirects(status_code=301) + session = client_session.Session(redirect=True) + req_resp = requests.get(self.REDIRECT_CHAIN[0], + allow_redirects=True) + + ses_resp = session.get(self.REDIRECT_CHAIN[0]) + + self.assertEqual(len(req_resp.history), len(ses_resp.history)) + + for r, s in zip(req_resp.history, ses_resp.history): + self.assertEqual(r.url, s.url) + self.assertEqual(r.status_code, s.status_code) + + +class ConstructSessionFromArgsTests(utils.TestCase): + + KEY = 'keyfile' + CERT = 'certfile' + CACERT = 'cacert-path' + + def _s(self, k=None, **kwargs): + k = k or kwargs + return client_session.Session.construct(k) + + def test_verify(self): + self.assertFalse(self._s(insecure=True).verify) + self.assertTrue(self._s(verify=True, insecure=True).verify) + self.assertFalse(self._s(verify=False, insecure=True).verify) + self.assertEqual(self._s(cacert=self.CACERT).verify, self.CACERT) + + def test_cert(self): + tup = (self.CERT, self.KEY) + self.assertEqual(self._s(cert=tup).cert, tup) + self.assertEqual(self._s(cert=self.CERT, key=self.KEY).cert, tup) + self.assertIsNone(self._s(key=self.KEY).cert) + + def test_pass_through(self): + value = 42 # only a number because timeout needs to be + for key in ['timeout', 'session', 'original_ip', 'user_agent']: + 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' + TEST_USER_ID = 'aUser' + TEST_PROJECT_ID = 'aProject' + + SERVICE_URLS = { + 'identity': {'public': 'http://identity-public:1111/v2.0', + 'admin': 'http://identity-admin:1111/v2.0'}, + 'compute': {'public': 'http://compute-public:2222/v1.0', + 'admin': 'http://compute-admin:2222/v1.0'}, + 'image': {'public': 'http://image-public:3333/v2.0', + 'admin': 'http://image-admin:3333/v2.0'} + } + + def __init__(self, token=TEST_TOKEN, invalidate=True): + self.token = token + self._invalidate = invalidate + + def get_token(self, session): + return self.token + + def get_endpoint(self, session, service_type=None, interface=None, + **kwargs): + try: + return self.SERVICE_URLS[service_type][interface] + except (KeyError, AttributeError): + return None + + def invalidate(self): + return self._invalidate + + def get_user_id(self, session): + return self.TEST_USER_ID + + def get_project_id(self, session): + return self.TEST_PROJECT_ID + + +class CalledAuthPlugin(base.BaseAuthPlugin): + + ENDPOINT = 'http://fakeendpoint/' + + def __init__(self, invalidate=True): + self.get_token_called = False + self.get_endpoint_called = False + self.endpoint_arguments = {} + self.invalidate_called = False + self._invalidate = invalidate + + def get_token(self, session): + self.get_token_called = True + return 'aToken' + + def get_endpoint(self, session, **kwargs): + self.get_endpoint_called = True + self.endpoint_arguments = kwargs + return self.ENDPOINT + + def invalidate(self): + self.invalidate_called = True + return self._invalidate + + +class SessionAuthTests(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/' + TEST_JSON = {'hello': 'world'} + + def stub_service_url(self, service_type, interface, path, + method='GET', **kwargs): + base_url = AuthPlugin.SERVICE_URLS[service_type][interface] + uri = "%s/%s" % (base_url.rstrip('/'), path.lstrip('/')) + + self.requests.register_uri(method, uri, **kwargs) + + 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) + + 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) + + def test_service_type_urls(self): + service_type = 'compute' + interface = 'public' + path = '/instances' + status = 200 + body = 'SUCCESS' + + self.stub_service_url(service_type=service_type, + interface=interface, + path=path, + status_code=status, + text=body) + + sess = client_session.Session(auth=AuthPlugin()) + resp = sess.get(path, + endpoint_filter={'service_type': service_type, + 'interface': interface}) + + self.assertEqual(self.requests.last_request.url, + AuthPlugin.SERVICE_URLS['compute']['public'] + path) + self.assertEqual(resp.text, body) + self.assertEqual(resp.status_code, status) + + def test_service_url_raises_if_no_auth_plugin(self): + sess = client_session.Session() + self.assertRaises(exceptions.MissingAuthPlugin, + sess.get, '/path', + endpoint_filter={'service_type': 'compute', + 'interface': 'public'}) + + def test_service_url_raises_if_no_url_returned(self): + sess = client_session.Session(auth=AuthPlugin()) + self.assertRaises(exceptions.EndpointNotFound, + sess.get, '/path', + endpoint_filter={'service_type': 'unknown', + 'interface': 'public'}) + + def test_raises_exc_only_when_asked(self): + # A request that returns a HTTP error should by default raise an + # exception by default, if you specify raise_exc=False then it will not + self.requests.get(self.TEST_URL, status_code=401) + + sess = client_session.Session() + self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL) + + resp = sess.get(self.TEST_URL, raise_exc=False) + self.assertEqual(401, resp.status_code) + + def test_passed_auth_plugin(self): + passed = CalledAuthPlugin() + sess = client_session.Session() + + self.requests.get(CalledAuthPlugin.ENDPOINT + 'path', + status_code=200) + endpoint_filter = {'service_type': 'identity'} + + # no plugin with authenticated won't work + self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path', + authenticated=True) + + # no plugin with an endpoint filter won't work + self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path', + authenticated=False, endpoint_filter=endpoint_filter) + + resp = sess.get('path', auth=passed, endpoint_filter=endpoint_filter) + + self.assertEqual(200, resp.status_code) + self.assertTrue(passed.get_endpoint_called) + self.assertTrue(passed.get_token_called) + + def test_passed_auth_plugin_overrides(self): + fixed = CalledAuthPlugin() + passed = CalledAuthPlugin() + + sess = client_session.Session(fixed) + + self.requests.get(CalledAuthPlugin.ENDPOINT + 'path', + status_code=200) + + resp = sess.get('path', auth=passed, + endpoint_filter={'service_type': 'identity'}) + + self.assertEqual(200, resp.status_code) + self.assertTrue(passed.get_endpoint_called) + self.assertTrue(passed.get_token_called) + self.assertFalse(fixed.get_endpoint_called) + self.assertFalse(fixed.get_token_called) + + def test_requests_auth_plugin(self): + sess = client_session.Session() + + requests_auth = object() + + FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'}) + RESP = mock.Mock(return_value=FAKE_RESP) + + with mock.patch.object(sess.session, 'request', RESP) as mocked: + sess.get(self.TEST_URL, requests_auth=requests_auth) + + mocked.assert_called_once_with('GET', self.TEST_URL, + headers=mock.ANY, + allow_redirects=mock.ANY, + auth=requests_auth, + verify=mock.ANY) + + def test_reauth_called(self): + auth = CalledAuthPlugin(invalidate=True) + sess = client_session.Session(auth=auth) + + self.requests.get(self.TEST_URL, + [{'text': 'Failed', 'status_code': 401}, + {'text': 'Hello', 'status_code': 200}]) + + # allow_reauth=True is the default + resp = sess.get(self.TEST_URL, authenticated=True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Hello', resp.text) + self.assertTrue(auth.invalidate_called) + + def test_reauth_not_called(self): + auth = CalledAuthPlugin(invalidate=True) + sess = client_session.Session(auth=auth) + + self.requests.get(self.TEST_URL, + [{'text': 'Failed', 'status_code': 401}, + {'text': 'Hello', 'status_code': 200}]) + + self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL, + authenticated=True, allow_reauth=False) + self.assertFalse(auth.invalidate_called) + + def test_endpoint_override_overrides_filter(self): + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + + override_base = 'http://mytest/' + path = 'path' + override_url = override_base + path + resp_text = uuid.uuid4().hex + + self.requests.get(override_url, text=resp_text) + + resp = sess.get(path, + endpoint_override=override_base, + endpoint_filter={'service_type': 'identity'}) + + self.assertEqual(resp_text, resp.text) + self.assertEqual(override_url, self.requests.last_request.url) + + self.assertTrue(auth.get_token_called) + self.assertFalse(auth.get_endpoint_called) + + def test_endpoint_override_ignore_full_url(self): + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + + path = 'path' + url = self.TEST_URL + path + + resp_text = uuid.uuid4().hex + self.requests.get(url, text=resp_text) + + resp = sess.get(url, + endpoint_override='http://someother.url', + endpoint_filter={'service_type': 'identity'}) + + self.assertEqual(resp_text, resp.text) + self.assertEqual(url, self.requests.last_request.url) + + self.assertTrue(auth.get_token_called) + self.assertFalse(auth.get_endpoint_called) + + def test_user_and_project_id(self): + auth = AuthPlugin() + sess = client_session.Session(auth=auth) + + self.assertEqual(auth.TEST_USER_ID, sess.get_user_id()) + self.assertEqual(auth.TEST_PROJECT_ID, sess.get_project_id()) + + +class AdapterTest(utils.TestCase): + + SERVICE_TYPE = uuid.uuid4().hex + SERVICE_NAME = uuid.uuid4().hex + INTERFACE = uuid.uuid4().hex + REGION_NAME = uuid.uuid4().hex + USER_AGENT = uuid.uuid4().hex + VERSION = uuid.uuid4().hex + + TEST_URL = CalledAuthPlugin.ENDPOINT + + def _create_loaded_adapter(self): + auth = CalledAuthPlugin() + sess = client_session.Session() + return adapter.Adapter(sess, + auth=auth, + service_type=self.SERVICE_TYPE, + service_name=self.SERVICE_NAME, + interface=self.INTERFACE, + region_name=self.REGION_NAME, + user_agent=self.USER_AGENT, + version=self.VERSION) + + def _verify_endpoint_called(self, adpt): + self.assertEqual(self.SERVICE_TYPE, + adpt.auth.endpoint_arguments['service_type']) + self.assertEqual(self.SERVICE_NAME, + adpt.auth.endpoint_arguments['service_name']) + self.assertEqual(self.INTERFACE, + adpt.auth.endpoint_arguments['interface']) + self.assertEqual(self.REGION_NAME, + adpt.auth.endpoint_arguments['region_name']) + self.assertEqual(self.VERSION, + adpt.auth.endpoint_arguments['version']) + + def test_setting_variables_on_request(self): + response = uuid.uuid4().hex + self.stub_url('GET', text=response) + adpt = self._create_loaded_adapter() + resp = adpt.get('/') + self.assertEqual(resp.text, response) + + self._verify_endpoint_called(adpt) + self.assertTrue(adpt.auth.get_token_called) + self.assertRequestHeaderEqual('User-Agent', self.USER_AGENT) + + def test_setting_variables_on_get_endpoint(self): + adpt = self._create_loaded_adapter() + url = adpt.get_endpoint() + + self.assertEqual(self.TEST_URL, url) + self._verify_endpoint_called(adpt) + + def test_legacy_binding(self): + key = uuid.uuid4().hex + val = uuid.uuid4().hex + response = jsonutils.dumps({key: val}) + + self.stub_url('GET', text=response) + + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.LegacyJsonAdapter(sess, + service_type=self.SERVICE_TYPE, + user_agent=self.USER_AGENT) + + resp, body = adpt.get('/') + self.assertEqual(self.SERVICE_TYPE, + auth.endpoint_arguments['service_type']) + self.assertEqual(resp.text, response) + self.assertEqual(val, body[key]) + + def test_legacy_binding_non_json_resp(self): + response = uuid.uuid4().hex + self.stub_url('GET', text=response, + headers={'Content-Type': 'text/html'}) + + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.LegacyJsonAdapter(sess, + service_type=self.SERVICE_TYPE, + user_agent=self.USER_AGENT) + + resp, body = adpt.get('/') + self.assertEqual(self.SERVICE_TYPE, + auth.endpoint_arguments['service_type']) + self.assertEqual(resp.text, response) + self.assertIsNone(body) + + def test_methods(self): + sess = client_session.Session() + adpt = adapter.Adapter(sess) + url = 'http://url' + + for method in ['get', 'head', 'post', 'put', 'patch', 'delete']: + with mock.patch.object(adpt, 'request') as m: + getattr(adpt, method)(url) + m.assert_called_once_with(url, method.upper()) + + def test_setting_endpoint_override(self): + endpoint_override = 'http://overrideurl' + path = '/path' + endpoint_url = endpoint_override + path + + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.Adapter(sess, endpoint_override=endpoint_override) + + response = uuid.uuid4().hex + self.requests.get(endpoint_url, text=response) + + resp = adpt.get(path) + + self.assertEqual(response, resp.text) + self.assertEqual(endpoint_url, self.requests.last_request.url) + + self.assertEqual(endpoint_override, adpt.get_endpoint()) + + def test_adapter_invalidate(self): + auth = CalledAuthPlugin() + sess = client_session.Session() + adpt = adapter.Adapter(sess, auth=auth) + + adpt.invalidate() + + self.assertTrue(auth.invalidate_called) + + def test_adapter_get_token(self): + auth = CalledAuthPlugin() + sess = client_session.Session() + adpt = adapter.Adapter(sess, auth=auth) + + self.assertEqual(self.TEST_TOKEN, adpt.get_token()) + self.assertTrue(auth.get_token_called) + + def test_adapter_connect_retries(self): + retries = 2 + sess = client_session.Session() + adpt = adapter.Adapter(sess, connect_retries=retries) + + def _refused_error(request, context): + raise requests.exceptions.ConnectionError() + + self.stub_url('GET', text=_refused_error) + + with mock.patch('time.sleep') as m: + self.assertRaises(exceptions.ConnectionRefused, + adpt.get, self.TEST_URL) + self.assertEqual(retries, m.call_count) + + # we count retries so there will be one initial request + 2 retries + self.assertThat(self.requests.request_history, + matchers.HasLength(retries + 1)) + + def test_user_and_project_id(self): + auth = AuthPlugin() + sess = client_session.Session() + adpt = adapter.Adapter(sess, auth=auth) + + self.assertEqual(auth.TEST_USER_ID, adpt.get_user_id()) + self.assertEqual(auth.TEST_PROJECT_ID, adpt.get_project_id()) + + +class ConfLoadingTests(utils.TestCase): + + GROUP = 'sessiongroup' + + def setUp(self): + super(ConfLoadingTests, self).setUp() + + self.conf_fixture = self.useFixture(config.Config()) + client_session.Session.register_conf_options(self.conf_fixture.conf, + self.GROUP) + + def config(self, **kwargs): + kwargs['group'] = self.GROUP + self.conf_fixture.config(**kwargs) + + def get_session(self, **kwargs): + return client_session.Session.load_from_conf_options( + self.conf_fixture.conf, + self.GROUP, + **kwargs) + + def test_insecure_timeout(self): + self.config(insecure=True, timeout=5) + s = self.get_session() + + self.assertFalse(s.verify) + self.assertEqual(5, s.timeout) + + def test_client_certs(self): + cert = '/path/to/certfile' + key = '/path/to/keyfile' + + self.config(certfile=cert, keyfile=key) + s = self.get_session() + + self.assertTrue(s.verify) + self.assertEqual((cert, key), s.cert) + + def test_cacert(self): + cafile = '/path/to/cacert' + + self.config(cafile=cafile) + s = self.get_session() + + self.assertEqual(cafile, s.verify) + + def test_deprecated(self): + def new_deprecated(): + return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex) + + opt_names = ['cafile', 'certfile', 'keyfile', 'insecure', 'timeout'] + depr = dict([(n, [new_deprecated()]) for n in opt_names]) + opts = client_session.Session.get_conf_options(deprecated_opts=depr) + + self.assertThat(opt_names, matchers.HasLength(len(opts))) + for opt in opts: + self.assertIn(depr[opt.name][0], opt.deprecated_opts) + + +class CliLoadingTests(utils.TestCase): + + def setUp(self): + super(CliLoadingTests, self).setUp() + + self.parser = argparse.ArgumentParser() + client_session.Session.register_cli_options(self.parser) + + def get_session(self, val, **kwargs): + args = self.parser.parse_args(val.split()) + return client_session.Session.load_from_cli_options(args, **kwargs) + + def test_insecure_timeout(self): + s = self.get_session('--insecure --timeout 5.5') + + self.assertFalse(s.verify) + self.assertEqual(5.5, s.timeout) + + def test_client_certs(self): + cert = '/path/to/certfile' + key = '/path/to/keyfile' + + s = self.get_session('--os-cert %s --os-key %s' % (cert, key)) + + self.assertTrue(s.verify) + self.assertEqual((cert, key), s.cert) + + def test_cacert(self): + cacert = '/path/to/cacert' + + s = self.get_session('--os-cacert %s' % cacert) + + self.assertEqual(cacert, s.verify) diff --git a/keystoneclient/tests/unit/test_shell.py b/keystoneclient/tests/unit/test_shell.py new file mode 100644 index 0000000..452f9e7 --- /dev/null +++ b/keystoneclient/tests/unit/test_shell.py @@ -0,0 +1,532 @@ +# 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 argparse +import json +import logging +import os +import sys +import uuid + +import fixtures +import mock +import six +import testtools +from testtools import matchers + +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient import shell as openstack_shell +from keystoneclient.tests.unit import utils +from keystoneclient.v2_0 import shell as shell_v2_0 + + +DEFAULT_USERNAME = 'username' +DEFAULT_PASSWORD = 'password' +DEFAULT_TENANT_ID = 'tenant_id' +DEFAULT_TENANT_NAME = 'tenant_name' +DEFAULT_AUTH_URL = 'http://127.0.0.1:5000/v2.0/' + + +# Make a fake shell object, a helping wrapper to call it +def shell(cmd): + openstack_shell.OpenStackIdentityShell().main(cmd.split()) + + +class NoExitArgumentParser(argparse.ArgumentParser): + def error(self, message): + raise exceptions.CommandError(message) + + +class ShellTest(utils.TestCase): + + FAKE_ENV = { + 'OS_USERNAME': DEFAULT_USERNAME, + 'OS_PASSWORD': DEFAULT_PASSWORD, + 'OS_TENANT_ID': DEFAULT_TENANT_ID, + 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, + 'OS_AUTH_URL': DEFAULT_AUTH_URL, + } + + def _tolerant_shell(self, cmd): + t_shell = openstack_shell.OpenStackIdentityShell(NoExitArgumentParser) + t_shell.main(cmd.split()) + + # Patch os.environ to avoid required auth info. + def setUp(self): + + super(ShellTest, self).setUp() + for var in os.environ: + if var.startswith("OS_"): + self.useFixture(fixtures.EnvironmentVariable(var, "")) + + for var in self.FAKE_ENV: + self.useFixture(fixtures.EnvironmentVariable(var, + self.FAKE_ENV[var])) + + def test_help_unknown_command(self): + self.assertRaises(exceptions.CommandError, shell, 'help %s' + % uuid.uuid4().hex) + + def shell(self, argstr): + orig = sys.stdout + clean_env = {} + _old_env, os.environ = os.environ, clean_env.copy() + try: + sys.stdout = six.StringIO() + _shell = openstack_shell.OpenStackIdentityShell() + _shell.main(argstr.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertEqual(exc_value.code, 0) + finally: + out = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + os.environ = _old_env + return out + + def test_help_no_args(self): + do_tenant_mock = mock.MagicMock() + with mock.patch('keystoneclient.shell.OpenStackIdentityShell.do_help', + do_tenant_mock): + self.shell('') + assert do_tenant_mock.called + + def test_help(self): + required = 'usage:' + help_text = self.shell('help') + self.assertThat(help_text, + matchers.MatchesRegex(required)) + + def test_help_command(self): + required = 'usage: keystone user-create' + help_text = self.shell('help user-create') + self.assertThat(help_text, + matchers.MatchesRegex(required)) + + def test_help_command_with_no_action_choices(self): + required = 'usage: keystone user-update' + help_text = self.shell('help user-update') + self.assertThat(help_text, + matchers.MatchesRegex(required)) + + def test_auth_no_credentials(self): + with testtools.ExpectedException( + exceptions.CommandError, 'Expecting'): + self.shell('user-list') + + def test_debug(self): + logging_mock = mock.MagicMock() + with mock.patch('logging.basicConfig', logging_mock): + self.assertRaises(exceptions.CommandError, + self.shell, '--debug user-list') + self.assertTrue(logging_mock.called) + self.assertEqual([(), {'level': logging.DEBUG}], + list(logging_mock.call_args)) + + def test_auth_password_authurl_no_username(self): + with testtools.ExpectedException( + exceptions.CommandError, + 'Expecting a username provided via either'): + self.shell('--os-password=%s --os-auth-url=%s user-list' + % (uuid.uuid4().hex, uuid.uuid4().hex)) + + def test_auth_username_password_no_authurl(self): + with testtools.ExpectedException( + exceptions.CommandError, 'Expecting an auth URL via either'): + self.shell('--os-password=%s --os-username=%s user-list' + % (uuid.uuid4().hex, uuid.uuid4().hex)) + + def test_token_no_endpoint(self): + with testtools.ExpectedException( + exceptions.CommandError, 'Expecting an endpoint provided'): + self.shell('--os-token=%s user-list' % uuid.uuid4().hex) + + def test_endpoint_no_token(self): + with testtools.ExpectedException( + exceptions.CommandError, 'Expecting a token provided'): + self.shell('--os-endpoint=http://10.0.0.1:5000/v2.0/ user-list') + + def test_shell_args(self): + do_tenant_mock = mock.MagicMock() + with mock.patch('keystoneclient.v2_0.shell.do_user_list', + do_tenant_mock): + shell('user-list') + assert do_tenant_mock.called + ((a, b), c) = do_tenant_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Old_style options + shell('--os_auth_url http://0.0.0.0:5000/ --os_password xyzpdq ' + '--os_tenant_id 1234 --os_tenant_name fred ' + '--os_username barney ' + '--os_identity_api_version 2.0 user-list') + assert do_tenant_mock.called + ((a, b), c) = do_tenant_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = ('http://0.0.0.0:5000/', 'xyzpdq', '1234', + 'fred', 'barney', '2.0') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + shell('--os-auth-url http://1.1.1.1:5000/ --os-password xyzpdq ' + '--os-tenant-id 4321 --os-tenant-name wilma ' + '--os-username betty ' + '--os-identity-api-version 2.0 user-list') + assert do_tenant_mock.called + ((a, b), c) = do_tenant_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = ('http://1.1.1.1:5000/', 'xyzpdq', '4321', + 'wilma', 'betty', '2.0') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Test keyring options + shell('--os-auth-url http://1.1.1.1:5000/ --os-password xyzpdq ' + '--os-tenant-id 4321 --os-tenant-name wilma ' + '--os-username betty ' + '--os-identity-api-version 2.0 ' + '--os-cache ' + '--stale-duration 500 ' + '--force-new-token user-list') + assert do_tenant_mock.called + ((a, b), c) = do_tenant_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version, b.os_cache, + b.stale_duration, b.force_new_token) + expect = ('http://1.1.1.1:5000/', 'xyzpdq', '4321', + 'wilma', 'betty', '2.0', True, '500', True) + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Test os-identity-api-version fall back to 2.0 + shell('--os-identity-api-version 3.0 user-list') + assert do_tenant_mock.called + self.assertTrue(b.os_identity_api_version, '2.0') + + def test_shell_user_create_args(self): + """Test user-create args.""" + do_uc_mock = mock.MagicMock() + # grab the decorators for do_user_create + uc_func = getattr(shell_v2_0, 'do_user_create') + do_uc_mock.arguments = getattr(uc_func, 'arguments', []) + with mock.patch('keystoneclient.v2_0.shell.do_user_create', + do_uc_mock): + + # Old_style options + # Test case with one --tenant_id args present: ec2 creds + shell('user-create --name=FOO ' + '--pass=secret --tenant_id=barrr --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.name, b.passwd, b.enabled) + expect = ('barrr', 'FOO', 'secret', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with one --tenant args present: ec2 creds + shell('user-create --name=foo ' + '--pass=secret --tenant=BARRR --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant, b.name, b.passwd, b.enabled) + expect = ('BARRR', 'foo', 'secret', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with one --tenant-id args present: ec2 creds + shell('user-create --name=foo ' + '--pass=secret --tenant-id=BARRR --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant, b.name, b.passwd, b.enabled) + expect = ('BARRR', 'foo', 'secret', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Old_style options + # Test case with --os_tenant_id and --tenant_id args present + shell('--os_tenant_id=os-tenant user-create --name=FOO ' + '--pass=secret --tenant_id=barrr --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'os-tenant', + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.name, b.passwd, b.enabled) + expect = ('barrr', 'FOO', 'secret', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with --os-tenant-id and --tenant-id args present + shell('--os-tenant-id=ostenant user-create --name=foo ' + '--pass=secret --tenant-id=BARRR --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'ostenant', + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant, b.name, b.passwd, b.enabled) + expect = ('BARRR', 'foo', 'secret', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + def test_do_tenant_create(self): + do_tenant_mock = mock.MagicMock() + with mock.patch('keystoneclient.v2_0.shell.do_tenant_create', + do_tenant_mock): + shell('tenant-create') + assert do_tenant_mock.called + # FIXME(dtroyer): how do you test the decorators? + # shell('tenant-create --tenant-name wilma ' + # '--description "fred\'s wife"') + # assert do_tenant_mock.called + + def test_do_tenant_list(self): + do_tenant_mock = mock.MagicMock() + with mock.patch('keystoneclient.v2_0.shell.do_tenant_list', + do_tenant_mock): + shell('tenant-list') + assert do_tenant_mock.called + + def test_shell_tenant_id_args(self): + """Test a corner case where --tenant_id appears on the + command-line twice. + """ + do_ec2_mock = mock.MagicMock() + # grab the decorators for do_ec2_create_credentials + ec2_func = getattr(shell_v2_0, 'do_ec2_credentials_create') + do_ec2_mock.arguments = getattr(ec2_func, 'arguments', []) + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_create', + do_ec2_mock): + + # Old_style options + # Test case with one --tenant_id args present: ec2 creds + shell('ec2-credentials-create ' + '--tenant_id=ec2-tenant --user_id=ec2-user') + assert do_ec2_mock.called + ((a, b), c) = do_ec2_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.user_id) + expect = ('ec2-tenant', 'ec2-user') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with one --tenant-id args present: ec2 creds + shell('ec2-credentials-create ' + '--tenant-id=dash-tenant --user-id=dash-user') + assert do_ec2_mock.called + ((a, b), c) = do_ec2_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.user_id) + expect = ('dash-tenant', 'dash-user') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Old_style options + # Test case with two --tenant_id args present + shell('--os_tenant_id=os-tenant ec2-credentials-create ' + '--tenant_id=ec2-tenant --user_id=ec2-user') + assert do_ec2_mock.called + ((a, b), c) = do_ec2_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'os-tenant', + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.user_id) + expect = ('ec2-tenant', 'ec2-user') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with two --tenant-id args present + shell('--os-tenant-id=ostenant ec2-credentials-create ' + '--tenant-id=dash-tenant --user-id=dash-user') + assert do_ec2_mock.called + ((a, b), c) = do_ec2_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'ostenant', + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.user_id) + expect = ('dash-tenant', 'dash-user') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + def test_do_ec2_get(self): + do_shell_mock = mock.MagicMock() + + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_create', + do_shell_mock): + shell('ec2-credentials-create') + assert do_shell_mock.called + + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_get', + do_shell_mock): + shell('ec2-credentials-get') + assert do_shell_mock.called + + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_list', + do_shell_mock): + shell('ec2-credentials-list') + assert do_shell_mock.called + + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_delete', + do_shell_mock): + shell('ec2-credentials-delete') + assert do_shell_mock.called + + def test_timeout_parse_invalid_type(self): + for f in ['foobar', 'xyz']: + cmd = '--timeout %s endpoint-create' % (f) + self.assertRaises(exceptions.CommandError, + self._tolerant_shell, cmd) + + def test_timeout_parse_invalid_number(self): + for f in [-1, 0]: + cmd = '--timeout %s endpoint-create' % (f) + self.assertRaises(exceptions.CommandError, + self._tolerant_shell, cmd) + + def test_do_timeout(self): + response_mock = mock.MagicMock() + response_mock.status_code = 200 + response_mock.text = json.dumps({ + 'endpoints': [], + }) + request_mock = mock.MagicMock(return_value=response_mock) + with mock.patch.object(session.requests, 'request', + request_mock): + shell(('--timeout 2 --os-token=blah --os-endpoint=blah' + ' --os-auth-url=blah.com endpoint-list')) + request_mock.assert_called_with(mock.ANY, mock.ANY, + timeout=2, + allow_redirects=False, + headers=mock.ANY, + verify=mock.ANY) + + def test_do_endpoints(self): + do_shell_mock = mock.MagicMock() + # grab the decorators for do_endpoint_create + shell_func = getattr(shell_v2_0, 'do_endpoint_create') + do_shell_mock.arguments = getattr(shell_func, 'arguments', []) + with mock.patch('keystoneclient.v2_0.shell.do_endpoint_create', + do_shell_mock): + + # Old_style options + # Test create args + shell('endpoint-create ' + '--service_id=2 --publicurl=http://example.com:1234/go ' + '--adminurl=http://example.com:9876/adm') + assert do_shell_mock.called + ((a, b), c) = do_shell_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.service, b.publicurl, b.adminurl) + expect = ('2', + 'http://example.com:1234/go', + 'http://example.com:9876/adm') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test create args + shell('endpoint-create ' + '--service-id=3 --publicurl=http://example.com:4321/go ' + '--adminurl=http://example.com:9876/adm') + assert do_shell_mock.called + ((a, b), c) = do_shell_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.service, b.publicurl, b.adminurl) + expect = ('3', + 'http://example.com:4321/go', + 'http://example.com:9876/adm') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test create args + shell('endpoint-create ' + '--service=3 --publicurl=http://example.com:4321/go ' + '--adminurl=http://example.com:9876/adm') + assert do_shell_mock.called + ((a, b), c) = do_shell_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.service, b.publicurl, b.adminurl) + expect = ('3', + 'http://example.com:4321/go', + 'http://example.com:9876/adm') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + def test_shell_keyboard_interrupt(self): + shell_mock = mock.MagicMock() + with mock.patch('keystoneclient.shell.OpenStackIdentityShell.main', + shell_mock): + try: + shell_mock.side_effect = KeyboardInterrupt() + openstack_shell.main() + except SystemExit as ex: + self.assertEqual(130, ex.code) diff --git a/keystoneclient/tests/unit/test_utils.py b/keystoneclient/tests/unit/test_utils.py new file mode 100644 index 0000000..8f0de9b --- /dev/null +++ b/keystoneclient/tests/unit/test_utils.py @@ -0,0 +1,240 @@ +# 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 +import sys + +import six +import testresources +from testtools import matchers + +from keystoneclient import exceptions +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit import utils as test_utils +from keystoneclient import utils + + +class FakeResource(object): + pass + + +class FakeManager(object): + + resource_class = FakeResource + + resources = { + '1234': {'name': 'entity_one'}, + '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0': {'name': 'entity_two'}, + '\xe3\x82\xbdtest': {'name': u'\u30bdtest'}, + '5678': {'name': '9876'} + } + + def get(self, resource_id): + try: + return self.resources[str(resource_id)] + except KeyError: + raise exceptions.NotFound(resource_id) + + def find(self, name=None): + if name == '9999': + # NOTE(morganfainberg): special case that raises NoUniqueMatch. + raise exceptions.NoUniqueMatch() + for resource_id, resource in self.resources.items(): + if resource['name'] == str(name): + return resource + raise exceptions.NotFound(name) + + +class FindResourceTestCase(test_utils.TestCase): + + def setUp(self): + super(FindResourceTestCase, self).setUp() + self.manager = FakeManager() + + def test_find_none(self): + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + 'asdf') + + def test_find_by_integer_id(self): + output = utils.find_resource(self.manager, 1234) + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_str_id(self): + output = utils.find_resource(self.manager, '1234') + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_uuid(self): + uuid = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' + output = utils.find_resource(self.manager, uuid) + self.assertEqual(output, self.manager.resources[uuid]) + + def test_find_by_unicode(self): + name = '\xe3\x82\xbdtest' + output = utils.find_resource(self.manager, name) + self.assertEqual(output, self.manager.resources[name]) + + def test_find_by_str_name(self): + output = utils.find_resource(self.manager, 'entity_one') + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_int_name(self): + output = utils.find_resource(self.manager, 9876) + self.assertEqual(output, self.manager.resources['5678']) + + def test_find_no_unique_match(self): + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + 9999) + + +class FakeObject(object): + def __init__(self, name): + self.name = name + + +class PrintTestCase(test_utils.TestCase): + def setUp(self): + super(PrintTestCase, self).setUp() + self.old_stdout = sys.stdout + self.stdout = six.moves.cStringIO() + self.addCleanup(setattr, self, 'stdout', None) + sys.stdout = self.stdout + self.addCleanup(setattr, sys, 'stdout', self.old_stdout) + + def test_print_list_unicode(self): + name = six.u('\u540d\u5b57') + objs = [FakeObject(name)] + # NOTE(Jeffrey4l) If the text's encode is proper, this method will not + # raise UnicodeEncodeError exceptions + utils.print_list(objs, ['name']) + output = self.stdout.getvalue() + # In Python 2, output will be bytes, while in Python 3, it will not. + # Let's decode the value if needed. + if isinstance(output, six.binary_type): + output = output.decode('utf-8') + self.assertIn(name, output) + + def test_print_dict_unicode(self): + name = six.u('\u540d\u5b57') + utils.print_dict({'name': name}) + output = self.stdout.getvalue() + # In Python 2, output will be bytes, while in Python 3, it will not. + # Let's decode the value if needed. + if isinstance(output, six.binary_type): + output = output.decode('utf-8') + self.assertIn(name, output) + + +class TestPositional(test_utils.TestCase): + + @utils.positional(1) + def no_vars(self): + # positional doesn't enforce anything here + return True + + @utils.positional(3, utils.positional.EXCEPT) + def mixed_except(self, arg, kwarg1=None, kwarg2=None): + # self, arg, and kwarg1 may be passed positionally + return (arg, kwarg1, kwarg2) + + @utils.positional(3, utils.positional.WARN) + def mixed_warn(self, arg, kwarg1=None, kwarg2=None): + # self, arg, and kwarg1 may be passed positionally, only a warning + # is emitted + return (arg, kwarg1, kwarg2) + + def test_nothing(self): + self.assertTrue(self.no_vars()) + + def test_mixed_except(self): + self.assertEqual((1, 2, 3), self.mixed_except(1, 2, kwarg2=3)) + self.assertEqual((1, 2, 3), self.mixed_except(1, kwarg1=2, kwarg2=3)) + self.assertEqual((1, None, None), self.mixed_except(1)) + self.assertRaises(TypeError, self.mixed_except, 1, 2, 3) + + def test_mixed_warn(self): + logger_message = six.moves.cStringIO() + handler = logging.StreamHandler(logger_message) + handler.setLevel(logging.DEBUG) + + logger = logging.getLogger(utils.__name__) + level = logger.getEffectiveLevel() + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + + self.addCleanup(logger.removeHandler, handler) + self.addCleanup(logger.setLevel, level) + + self.mixed_warn(1, 2, 3) + + self.assertIn('takes at most 3 positional', logger_message.getvalue()) + + @utils.positional(enforcement=utils.positional.EXCEPT) + def inspect_func(self, arg, kwarg=None): + return (arg, kwarg) + + def test_inspect_positions(self): + self.assertEqual((1, None), self.inspect_func(1)) + self.assertEqual((1, 2), self.inspect_func(1, kwarg=2)) + self.assertRaises(TypeError, self.inspect_func) + self.assertRaises(TypeError, self.inspect_func, 1, 2) + + @utils.positional.classmethod(1) + def class_method(cls, a, b): + return (cls, a, b) + + @utils.positional.method(1) + def normal_method(self, a, b): + self.assertIsInstance(self, TestPositional) + return (self, a, b) + + def test_class_method(self): + self.assertEqual((TestPositional, 1, 2), self.class_method(1, b=2)) + self.assertRaises(TypeError, self.class_method, 1, 2) + + def test_normal_method(self): + self.assertEqual((self, 1, 2), self.normal_method(1, b=2)) + self.assertRaises(TypeError, self.normal_method, 1, 2) + + +class HashSignedTokenTestCase(test_utils.TestCase, + testresources.ResourcedTestCase): + """Unit tests for utils.hash_signed_token().""" + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_default_md5(self): + """The default hash method is md5.""" + token = self.examples.SIGNED_TOKEN_SCOPED + if six.PY3: + token = token.encode('utf-8') + token_id_default = utils.hash_signed_token(token) + token_id_md5 = utils.hash_signed_token(token, mode='md5') + self.assertThat(token_id_default, matchers.Equals(token_id_md5)) + # md5 hash is 32 chars. + self.assertThat(token_id_default, matchers.HasLength(32)) + + def test_sha256(self): + """Can also hash with sha256.""" + token = self.examples.SIGNED_TOKEN_SCOPED + if six.PY3: + token = token.encode('utf-8') + token_id = utils.hash_signed_token(token, mode='sha256') + # sha256 hash is 64 chars. + self.assertThat(token_id, matchers.HasLength(64)) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/utils.py b/keystoneclient/tests/unit/utils.py new file mode 100644 index 0000000..038f34c --- /dev/null +++ b/keystoneclient/tests/unit/utils.py @@ -0,0 +1,209 @@ +# 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 +import sys +import time +import uuid + +import fixtures +import mock +from mox3 import mox +from oslo_serialization import jsonutils +import requests +from requests_mock.contrib import fixture +import six +from six.moves.urllib import parse as urlparse +import testtools + + +class TestCase(testtools.TestCase): + + TEST_DOMAIN_ID = '1' + TEST_DOMAIN_NAME = 'aDomain' + TEST_GROUP_ID = uuid.uuid4().hex + TEST_ROLE_ID = uuid.uuid4().hex + TEST_TENANT_ID = '1' + TEST_TENANT_NAME = 'aTenant' + TEST_TOKEN = 'aToken' + TEST_TRUST_ID = 'aTrust' + TEST_USER = 'test' + TEST_USER_ID = uuid.uuid4().hex + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + + def setUp(self): + super(TestCase, self).setUp() + self.mox = mox.Mox() + self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) + self.time_patcher = mock.patch.object(time, 'time', lambda: 1234) + self.time_patcher.start() + + self.requests = self.useFixture(fixture.Fixture()) + + def tearDown(self): + self.time_patcher.stop() + self.mox.UnsetStubs() + self.mox.VerifyAll() + super(TestCase, self).tearDown() + + def stub_url(self, method, parts=None, base_url=None, json=None, **kwargs): + if not base_url: + base_url = self.TEST_URL + + if json: + kwargs['text'] = jsonutils.dumps(json) + headers = kwargs.setdefault('headers', {}) + headers['Content-Type'] = 'application/json' + + if parts: + url = '/'.join([p.strip('/') for p in [base_url] + parts]) + else: + url = base_url + + url = url.replace("/?", "?") + self.requests.register_uri(method, url, **kwargs) + + def assertRequestBodyIs(self, body=None, json=None): + last_request_body = self.requests.last_request.body + if json: + val = jsonutils.loads(last_request_body) + self.assertEqual(json, val) + elif body: + self.assertEqual(body, last_request_body) + + def assertQueryStringIs(self, qs=''): + """Verify the QueryString matches what is expected. + + The qs parameter should be of the format \'foo=bar&abc=xyz\' + """ + expected = urlparse.parse_qs(qs, keep_blank_values=True) + parts = urlparse.urlparse(self.requests.last_request.url) + querystring = urlparse.parse_qs(parts.query, keep_blank_values=True) + self.assertEqual(expected, querystring) + + def assertQueryStringContains(self, **kwargs): + """Verify the query string contains the expected parameters. + + This method is used to verify that the query string for the most recent + request made contains all the parameters provided as ``kwargs``, and + that the value of each parameter contains the value for the kwarg. If + the value for the kwarg is an empty string (''), then all that's + verified is that the parameter is present. + + """ + parts = urlparse.urlparse(self.requests.last_request.url) + qs = urlparse.parse_qs(parts.query, keep_blank_values=True) + + for k, v in six.iteritems(kwargs): + self.assertIn(k, qs) + self.assertIn(v, qs[k]) + + def assertRequestHeaderEqual(self, name, val): + """Verify that the last request made contains a header and its value + + The request must have already been made. + """ + headers = self.requests.last_request.headers + self.assertEqual(headers.get(name), val) + + +if tuple(sys.version_info)[0:2] < (2, 7): + + def assertDictEqual(self, d1, d2, msg=None): + # Simple version taken from 2.7 + self.assertIsInstance(d1, dict, + 'First argument is not a dictionary') + self.assertIsInstance(d2, dict, + 'Second argument is not a dictionary') + if d1 != d2: + if msg: + self.fail(msg) + else: + standardMsg = '%r != %r' % (d1, d2) + self.fail(standardMsg) + + TestCase.assertDictEqual = assertDictEqual + + +class TestResponse(requests.Response): + """Class used to wrap requests.Response and provide some + convenience to initialize with a dict. + """ + + def __init__(self, data): + self._text = None + super(TestResponse, self).__init__() + if isinstance(data, dict): + self.status_code = data.get('status_code', 200) + headers = data.get('headers') + if headers: + self.headers.update(headers) + # Fake the text attribute to streamline Response creation + # _content is defined by requests.Response + self._content = data.get('text') + else: + self.status_code = data + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @property + def text(self): + return self.content + + +class DisableModuleFixture(fixtures.Fixture): + """A fixture to provide support for unloading/disabling modules.""" + + def __init__(self, module, *args, **kw): + super(DisableModuleFixture, self).__init__(*args, **kw) + self.module = module + self._finders = [] + self._cleared_modules = {} + + def tearDown(self): + super(DisableModuleFixture, self).tearDown() + for finder in self._finders: + sys.meta_path.remove(finder) + sys.modules.update(self._cleared_modules) + + def clear_module(self): + cleared_modules = {} + for fullname in sys.modules.keys(): + if (fullname == self.module or + fullname.startswith(self.module + '.')): + cleared_modules[fullname] = sys.modules.pop(fullname) + return cleared_modules + + def setUp(self): + """Ensure ImportError for the specified module.""" + + super(DisableModuleFixture, self).setUp() + + # Clear 'module' references in sys.modules + self._cleared_modules.update(self.clear_module()) + + finder = NoModuleFinder(self.module) + self._finders.append(finder) + sys.meta_path.insert(0, finder) + + +class NoModuleFinder(object): + """Disallow further imports of 'module'.""" + + def __init__(self, module): + self.module = module + + def find_module(self, fullname, path): + if fullname == self.module or fullname.startswith(self.module + '.'): + raise ImportError diff --git a/keystoneclient/tests/unit/v2_0/__init__.py b/keystoneclient/tests/unit/v2_0/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/__init__.py diff --git a/keystoneclient/tests/unit/v2_0/client_fixtures.py b/keystoneclient/tests/unit/v2_0/client_fixtures.py new file mode 100644 index 0000000..39d808e --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/client_fixtures.py @@ -0,0 +1,124 @@ +# 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 __future__ import unicode_literals + +from keystoneclient import fixture + + +def unscoped_token(): + return fixture.V2Token(token_id='3e2813b7ba0b4006840c3825860b86ed', + expires='2012-10-03T16:58:01Z', + user_id='c4da488862bd435c9e6c0275a0d0e49a', + user_name='exampleuser') + + +def project_scoped_token(): + _TENANT_ID = '225da22d3ce34b15877ea70b2a575f58' + + f = fixture.V2Token(token_id='04c7d5ffaeef485f9dc69c06db285bdb', + expires='2012-10-03T16:53:36Z', + tenant_id='225da22d3ce34b15877ea70b2a575f58', + tenant_name='exampleproject', + user_id='c4da488862bd435c9e6c0275a0d0e49a', + user_name='exampleuser') + + f.add_role(id='member_id', name='Member') + + s = f.add_service('volume', 'Volume Service') + s.add_endpoint(public='http://public.com:8776/v1/%s' % _TENANT_ID, + admin='http://admin:8776/v1/%s' % _TENANT_ID, + internal='http://internal:8776/v1/%s' % _TENANT_ID, + region='RegionOne') + + s = f.add_service('image', 'Image Service') + s.add_endpoint(public='http://public.com:9292/v1', + admin='http://admin:9292/v1', + internal='http://internal:9292/v1', + region='RegionOne') + + s = f.add_service('compute', 'Compute Service') + s.add_endpoint(public='http://public.com:8774/v2/%s' % _TENANT_ID, + admin='http://admin:8774/v2/%s' % _TENANT_ID, + internal='http://internal:8774/v2/%s' % _TENANT_ID, + region='RegionOne') + + s = f.add_service('ec2', 'EC2 Service') + s.add_endpoint(public='http://public.com:8773/services/Cloud', + admin='http://admin:8773/services/Admin', + internal='http://internal:8773/services/Cloud', + region='RegionOne') + + s = f.add_service('identity', 'Identity Service') + s.add_endpoint(public='http://public.com:5000/v2.0', + admin='http://admin:35357/v2.0', + internal='http://internal:5000/v2.0', + region='RegionOne') + + return f + + +def auth_response_body(): + f = fixture.V2Token(token_id='ab48a9efdfedb23ty3494', + expires='2010-11-01T03:32:15-05:00', + tenant_id='345', + tenant_name='My Project', + user_id='123', + user_name='jqsmith') + + f.add_role(id='234', name='compute:admin') + role = f.add_role(id='235', name='object-store:admin') + role['tenantId'] = '1' + + s = f.add_service('compute', 'Cloud Servers') + endpoint = s.add_endpoint(public='https://compute.north.host/v1/1234', + internal='https://compute.north.host/v1/1234', + region='North') + endpoint['tenantId'] = '1' + endpoint['versionId'] = '1.0' + endpoint['versionInfo'] = 'https://compute.north.host/v1.0/' + endpoint['versionList'] = 'https://compute.north.host/' + + endpoint = s.add_endpoint(public='https://compute.north.host/v1.1/3456', + internal='https://compute.north.host/v1.1/3456', + region='North') + endpoint['tenantId'] = '2' + endpoint['versionId'] = '1.1' + endpoint['versionInfo'] = 'https://compute.north.host/v1.1/' + endpoint['versionList'] = 'https://compute.north.host/' + + s = f.add_service('object-store', 'Cloud Files') + endpoint = s.add_endpoint(public='https://swift.north.host/v1/blah', + internal='https://swift.north.host/v1/blah', + region='South') + endpoint['tenantId'] = '11' + endpoint['versionId'] = '1.0' + endpoint['versionInfo'] = 'uri' + endpoint['versionList'] = 'uri' + + endpoint = s.add_endpoint(public='https://swift.north.host/v1.1/blah', + internal='https://compute.north.host/v1.1/blah', + region='South') + endpoint['tenantId'] = '2' + endpoint['versionId'] = '1.1' + endpoint['versionInfo'] = 'https://swift.north.host/v1.1/' + endpoint['versionList'] = 'https://swift.north.host/' + + s = f.add_service('image', 'Image Servers') + s.add_endpoint(public='https://image.north.host/v1/', + internal='https://image-internal.north.host/v1/', + region='North') + s.add_endpoint(public='https://image.south.host/v1/', + internal='https://image-internal.south.host/v1/', + region='South') + + return f diff --git a/keystoneclient/tests/unit/v2_0/test_access.py b/keystoneclient/tests/unit/v2_0/test_access.py new file mode 100644 index 0000000..f05f138 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_access.py @@ -0,0 +1,201 @@ +# 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 datetime +import uuid + +from oslo_utils import timeutils +import testresources + +from keystoneclient import access +from keystoneclient import fixture +from keystoneclient.tests.unit import client_fixtures as token_data +from keystoneclient.tests.unit.v2_0 import client_fixtures +from keystoneclient.tests.unit.v2_0 import utils + + +class AccessInfoTest(utils.TestCase, testresources.ResourcedTestCase): + + resources = [('examples', token_data.EXAMPLES_RESOURCE)] + + def test_building_unscoped_accessinfo(self): + token = client_fixtures.unscoped_token() + auth_ref = access.AccessInfo.factory(body=token) + + self.assertTrue(auth_ref) + self.assertIn('token', auth_ref) + + self.assertEqual(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEqual(auth_ref.username, 'exampleuser') + self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEqual(auth_ref.role_ids, []) + self.assertEqual(auth_ref.role_names, []) + + self.assertIsNone(auth_ref.tenant_name) + self.assertIsNone(auth_ref.tenant_id) + + self.assertIsNone(auth_ref.auth_url) + self.assertIsNone(auth_ref.management_url) + + self.assertFalse(auth_ref.scoped) + self.assertFalse(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + self.assertFalse(auth_ref.trust_scoped) + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + + self.assertEqual(auth_ref.expires, token.expires) + self.assertEqual(auth_ref.issued, token.issued) + + def test_will_expire_soon(self): + token = client_fixtures.unscoped_token() + expires = timeutils.utcnow() + datetime.timedelta(minutes=5) + token.expires = expires + auth_ref = access.AccessInfo.factory(body=token) + self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) + self.assertTrue(auth_ref.will_expire_soon(stale_duration=300)) + self.assertFalse(auth_ref.will_expire_soon()) + + def test_building_scoped_accessinfo(self): + token = client_fixtures.project_scoped_token() + auth_ref = access.AccessInfo.factory(body=token) + + self.assertTrue(auth_ref) + self.assertIn('token', auth_ref) + self.assertIn('serviceCatalog', auth_ref) + self.assertTrue(auth_ref['serviceCatalog']) + + self.assertEqual(auth_ref.auth_token, + '04c7d5ffaeef485f9dc69c06db285bdb') + self.assertEqual(auth_ref.username, 'exampleuser') + self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEqual(auth_ref.role_ids, ['member_id']) + self.assertEqual(auth_ref.role_names, ['Member']) + + self.assertEqual(auth_ref.tenant_name, 'exampleproject') + self.assertEqual(auth_ref.tenant_id, + '225da22d3ce34b15877ea70b2a575f58') + + self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) + self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) + + self.assertEqual(auth_ref.auth_url, ('http://public.com:5000/v2.0',)) + self.assertEqual(auth_ref.management_url, ('http://admin:35357/v2.0',)) + + self.assertEqual(auth_ref.project_domain_id, 'default') + self.assertEqual(auth_ref.project_domain_name, 'Default') + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + + self.assertTrue(auth_ref.scoped) + self.assertTrue(auth_ref.project_scoped) + self.assertFalse(auth_ref.domain_scoped) + + def test_diablo_token(self): + diablo_token = self.examples.TOKEN_RESPONSES[ + self.examples.VALID_DIABLO_TOKEN] + auth_ref = access.AccessInfo.factory(body=diablo_token) + + self.assertTrue(auth_ref) + self.assertEqual(auth_ref.username, 'user_name1') + self.assertEqual(auth_ref.project_id, 'tenant_id1') + self.assertEqual(auth_ref.project_name, 'tenant_id1') + self.assertEqual(auth_ref.project_domain_id, 'default') + self.assertEqual(auth_ref.project_domain_name, 'Default') + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + self.assertEqual(auth_ref.role_names, ['role1', 'role2']) + self.assertFalse(auth_ref.scoped) + + def test_grizzly_token(self): + grizzly_token = self.examples.TOKEN_RESPONSES[ + self.examples.SIGNED_TOKEN_SCOPED_KEY] + auth_ref = access.AccessInfo.factory(body=grizzly_token) + + self.assertEqual(auth_ref.project_id, 'tenant_id1') + self.assertEqual(auth_ref.project_name, 'tenant_name1') + self.assertEqual(auth_ref.project_domain_id, 'default') + self.assertEqual(auth_ref.project_domain_name, 'Default') + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + self.assertEqual(auth_ref.role_names, ['role1', 'role2']) + + def test_v2_roles(self): + role_id = 'a' + role_name = 'b' + + token = fixture.V2Token() + token.set_scope() + token.add_role(id=role_id, name=role_name) + + auth_ref = access.AccessInfo.factory(body=token) + + self.assertEqual([role_id], auth_ref.role_ids) + self.assertEqual([role_id], auth_ref['metadata']['roles']) + self.assertEqual([role_name], auth_ref.role_names) + self.assertEqual([{'name': role_name}], auth_ref['user']['roles']) + + def test_trusts(self): + user_id = uuid.uuid4().hex + trust_id = uuid.uuid4().hex + + token = fixture.V2Token(user_id=user_id, trust_id=trust_id) + token.set_scope() + token.add_role() + + auth_ref = access.AccessInfo.factory(body=token) + + self.assertEqual(trust_id, auth_ref.trust_id) + self.assertEqual(user_id, auth_ref.trustee_user_id) + + self.assertEqual(trust_id, token['access']['trust']['id']) + + def test_override_auth_token(self): + token = fixture.V2Token() + token.set_scope() + token.add_role() + + new_auth_token = uuid.uuid4().hex + + auth_ref = access.AccessInfo.factory(body=token) + + self.assertEqual(token.token_id, auth_ref.auth_token) + + auth_ref.auth_token = new_auth_token + self.assertEqual(new_auth_token, auth_ref.auth_token) + + del auth_ref.auth_token + self.assertEqual(token.token_id, auth_ref.auth_token) + + def test_override_auth_token_in_factory(self): + token = fixture.V2Token() + token.set_scope() + token.add_role() + + new_auth_token = uuid.uuid4().hex + + auth_ref = access.AccessInfo.factory(body=token, + auth_token=new_auth_token) + + self.assertEqual(new_auth_token, auth_ref.auth_token) + del auth_ref.auth_token + self.assertEqual(token.token_id, auth_ref.auth_token) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/v2_0/test_auth.py b/keystoneclient/tests/unit/v2_0/test_auth.py new file mode 100644 index 0000000..e61f5c8 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_auth.py @@ -0,0 +1,258 @@ +# 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 copy +import datetime + +from oslo_serialization import jsonutils +from oslo_utils import timeutils + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import client + + +class AuthenticateAgainstKeystoneTests(utils.TestCase): + def setUp(self): + super(AuthenticateAgainstKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "access": { + "token": { + "expires": "2020-01-01T00:00:10.000123Z", + "id": self.TEST_TOKEN, + "tenant": { + "id": self.TEST_TENANT_ID + }, + }, + "user": { + "id": self.TEST_USER + }, + "serviceCatalog": self.TEST_SERVICE_CATALOG, + }, + } + self.TEST_REQUEST_BODY = { + "auth": { + "passwordCredentials": { + "username": self.TEST_USER, + "password": self.TEST_TOKEN, + }, + "tenantId": self.TEST_TENANT_ID, + }, + } + + def test_authenticate_success_expired(self): + resp_a = copy.deepcopy(self.TEST_RESPONSE_DICT) + resp_b = copy.deepcopy(self.TEST_RESPONSE_DICT) + headers = {'Content-Type': 'application/json'} + + # Build an expired token + resp_a['access']['token']['expires'] = ( + (timeutils.utcnow() - datetime.timedelta(1)).isoformat()) + + # Build a new response + TEST_TOKEN = "abcdef" + resp_b['access']['token']['expires'] = '2020-01-01T00:00:10.000123Z' + resp_b['access']['token']['id'] = TEST_TOKEN + + # return expired first, and then the new response + self.stub_auth(response_list=[{'json': resp_a, 'headers': headers}, + {'json': resp_b, 'headers': headers}]) + + cs = client.Client(tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_TOKEN) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + + self.assertEqual(cs.auth_token, TEST_TOKEN) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_failure(self): + _auth = 'auth' + _cred = 'passwordCredentials' + _pass = 'password' + self.TEST_REQUEST_BODY[_auth][_cred][_pass] = 'bad_key' + error = {"unauthorized": {"message": "Unauthorized", + "code": "401"}} + + self.stub_auth(status_code=401, json=error) + + # Workaround for issue with assertRaises on python2.6 + # where with assertRaises(exceptions.Unauthorized): doesn't work + # right + def client_create_wrapper(): + client.Client(username=self.TEST_USER, + password="bad_key", + tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertRaises(exceptions.Unauthorized, client_create_wrapper) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_auth_redirect(self): + self.stub_auth(status_code=305, text='Use Proxy', + headers={'Location': self.TEST_ADMIN_URL + "/tokens"}) + + self.stub_auth(base_url=self.TEST_ADMIN_URL, + json=self.TEST_RESPONSE_DICT) + + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_password_unscoped(self): + del self.TEST_RESPONSE_DICT['access']['serviceCatalog'] + del self.TEST_REQUEST_BODY['auth']['tenantId'] + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertFalse('serviceCatalog' in cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_auth_url_token_authentication(self): + fake_token = 'fake_token' + fake_url = '/fake-url' + fake_resp = {'result': True} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [fake_url], json=fake_resp, + base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) + + cl = client.Client(auth_url=self.TEST_URL, + token=fake_token) + json_body = jsonutils.loads(self.requests.last_request.body) + self.assertEqual(json_body['auth']['token']['id'], fake_token) + + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) + + def test_authenticate_success_token_scoped(self): + del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] + self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_token_scoped_trust(self): + del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] + self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} + self.TEST_REQUEST_BODY['auth']['trust_id'] = self.TEST_TRUST_ID + response = self.TEST_RESPONSE_DICT.copy() + response['access']['trust'] = {"trustee_user_id": self.TEST_USER, + "id": self.TEST_TRUST_ID} + self.stub_auth(json=response) + + cs = client.Client(token=self.TEST_TOKEN, + tenant_id=self.TEST_TENANT_ID, + trust_id=self.TEST_TRUST_ID, + auth_url=self.TEST_URL) + self.assertTrue(cs.auth_ref.trust_scoped) + self.assertEqual(cs.auth_ref.trust_id, self.TEST_TRUST_ID) + self.assertEqual(cs.auth_ref.trustee_user_id, self.TEST_USER) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_token_unscoped(self): + del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] + del self.TEST_REQUEST_BODY['auth']['tenantId'] + del self.TEST_RESPONSE_DICT['access']['serviceCatalog'] + self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertFalse('serviceCatalog' in cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_allow_override_of_auth_token(self): + fake_url = '/fake-url' + fake_token = 'fake_token' + fake_resp = {'result': True} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [fake_url], json=fake_resp, + base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + + self.assertEqual(cl.auth_token, self.TEST_TOKEN) + + # the token returned from the authentication will be used + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) + + # then override that token and the new token shall be used + cl.auth_token = fake_token + + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests.last_request.headers.get('X-Auth-Token') + self.assertEqual(fake_token, token) + + # if we clear that overridden token then we fall back to the original + del cl.auth_token + + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) diff --git a/keystoneclient/tests/unit/v2_0/test_certificates.py b/keystoneclient/tests/unit/v2_0/test_certificates.py new file mode 100644 index 0000000..fc19d81 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_certificates.py @@ -0,0 +1,40 @@ +# Copyright 2014 IBM Corp. +# 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 testresources + +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit.v2_0 import utils + + +class CertificateTests(utils.TestCase, testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_get_ca_certificate(self): + self.stub_url('GET', ['certificates', 'ca'], + headers={'Content-Type': 'text/html; charset=UTF-8'}, + text=self.examples.SIGNING_CA) + res = self.client.certificates.get_ca_certificate() + self.assertEqual(self.examples.SIGNING_CA, res) + + def test_get_signing_certificate(self): + self.stub_url('GET', ['certificates', 'signing'], + headers={'Content-Type': 'text/html; charset=UTF-8'}, + text=self.examples.SIGNING_CERT) + res = self.client.certificates.get_signing_certificate() + self.assertEqual(self.examples.SIGNING_CERT, res) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/v2_0/test_client.py b/keystoneclient/tests/unit/v2_0/test_client.py new file mode 100644 index 0000000..2700b31 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_client.py @@ -0,0 +1,184 @@ +# 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 json +import uuid + +import six + +from keystoneclient.auth import token_endpoint +from keystoneclient import exceptions +from keystoneclient import fixture +from keystoneclient import session +from keystoneclient.tests.unit.v2_0 import client_fixtures +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import client + + +class KeystoneClientTest(utils.TestCase): + + def test_unscoped_init(self): + token = client_fixtures.unscoped_token() + self.stub_auth(json=token) + + c = client.Client(username='exampleuser', + password='password', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.scoped) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertIsNone(c.auth_ref.trust_id) + self.assertFalse(c.auth_ref.trust_scoped) + self.assertIsNone(c.get_project_id(session=None)) + self.assertEqual(token.user_id, c.get_user_id(session=None)) + + def test_scoped_init(self): + token = client_fixtures.project_scoped_token() + self.stub_auth(json=token) + + c = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertTrue(c.auth_ref.scoped) + self.assertTrue(c.auth_ref.project_scoped) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertIsNone(c.auth_ref.trust_id) + self.assertFalse(c.auth_ref.trust_scoped) + + self.assertEqual(token.tenant_id, c.get_project_id(session=None)) + self.assertEqual(token.user_id, c.get_user_id(session=None)) + + def test_auth_ref_load(self): + self.stub_auth(json=client_fixtures.project_scoped_token()) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + cache = json.dumps(cl.auth_ref) + new_client = client.Client(auth_ref=json.loads(cache)) + self.assertIsNotNone(new_client.auth_ref) + self.assertTrue(new_client.auth_ref.scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertIsNone(new_client.auth_ref.trust_id) + self.assertFalse(new_client.auth_ref.trust_scoped) + self.assertEqual(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v2.0') + + def test_auth_ref_load_with_overridden_arguments(self): + self.stub_auth(json=client_fixtures.project_scoped_token()) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + cache = json.dumps(cl.auth_ref) + new_auth_url = "http://new-public:5000/v2.0" + new_client = client.Client(auth_ref=json.loads(cache), + auth_url=new_auth_url) + self.assertIsNotNone(new_client.auth_ref) + self.assertTrue(new_client.auth_ref.scoped) + self.assertTrue(new_client.auth_ref.scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertIsNone(new_client.auth_ref.trust_id) + self.assertFalse(new_client.auth_ref.trust_scoped) + self.assertEqual(new_client.auth_url, new_auth_url) + self.assertEqual(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v2.0') + + def test_init_err_no_auth_url(self): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + username='exampleuser', + password='password') + + def test_management_url_is_updated(self): + first = fixture.V2Token() + first.set_scope() + admin_url = 'http://admin:35357/v2.0' + second_url = 'http://secondurl:35357/v2.0' + + s = first.add_service('identity') + s.add_endpoint(public='http://public.com:5000/v2.0', + admin=admin_url) + + second = fixture.V2Token() + second.set_scope() + s = second.add_service('identity') + s.add_endpoint(public='http://secondurl:5000/v2.0', + admin=second_url) + + self.stub_auth(response_list=[{'json': first}, {'json': second}]) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + self.assertEqual(cl.management_url, admin_url) + + cl.authenticate() + self.assertEqual(cl.management_url, second_url) + + def test_client_with_region_name_passes_to_service_catalog(self): + # NOTE(jamielennox): this is deprecated behaviour that should be + # removed ASAP, however must remain compatible. + self.stub_auth(json=client_fixtures.auth_response_body()) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL, + region_name='North') + self.assertEqual(cl.service_catalog.url_for(service_type='image'), + 'https://image.north.host/v1/') + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL, + region_name='South') + self.assertEqual(cl.service_catalog.url_for(service_type='image'), + 'https://image.south.host/v1/') + + def test_client_without_auth_params(self): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + tenant_name='exampleproject', + auth_url=self.TEST_URL) + + def test_client_params(self): + opts = {'auth': token_endpoint.Token('a', 'b'), + 'connect_retries': 50, + 'endpoint_override': uuid.uuid4().hex, + 'interface': uuid.uuid4().hex, + 'region_name': uuid.uuid4().hex, + 'service_name': uuid.uuid4().hex, + 'user_agent': uuid.uuid4().hex, + } + + sess = session.Session() + cl = client.Client(session=sess, **opts) + + for k, v in six.iteritems(opts): + self.assertEqual(v, getattr(cl._adapter, k)) + + self.assertEqual('identity', cl._adapter.service_type) + self.assertEqual('v2.0', cl._adapter.version) diff --git a/keystoneclient/tests/unit/v2_0/test_discovery.py b/keystoneclient/tests/unit/v2_0/test_discovery.py new file mode 100644 index 0000000..348038a --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_discovery.py @@ -0,0 +1,80 @@ +# 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.generic import client +from keystoneclient.tests.unit.v2_0 import utils + + +class DiscoverKeystoneTests(utils.UnauthenticatedTestCase): + def setUp(self): + super(DiscoverKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "versions": { + "values": [{ + "id": "v2.0", + "status": "beta", + "updated": "2011-11-19T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:5000/v2.0/", }, + {"rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/content/", }, + {"rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/" + "identity-dev-guide-2.0.pdf", }, + {"rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://127.0.0.1:5000/v2.0/identity.wadl", } + ], + "media-types": [{ + "base": "application/xml", + "type": "application/vnd.openstack.identity-v2.0+xml", + }, { + "base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json", + }], + }], + }, + } + + def test_get_versions(self): + self.stub_url('GET', base_url=self.TEST_ROOT_URL, + json=self.TEST_RESPONSE_DICT) + + cs = client.Client() + versions = cs.discover(self.TEST_ROOT_URL) + self.assertIsInstance(versions, dict) + self.assertIn('message', versions) + self.assertIn('v2.0', versions) + self.assertEqual( + versions['v2.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] + ['href']) + + def test_get_version_local(self): + self.stub_url('GET', base_url="http://localhost:35357/", + json=self.TEST_RESPONSE_DICT) + + cs = client.Client() + versions = cs.discover() + self.assertIsInstance(versions, dict) + self.assertIn('message', versions) + self.assertIn('v2.0', versions) + self.assertEqual( + versions['v2.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] + ['href']) diff --git a/keystoneclient/tests/unit/v2_0/test_ec2.py b/keystoneclient/tests/unit/v2_0/test_ec2.py new file mode 100644 index 0000000..e08d228 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_ec2.py @@ -0,0 +1,107 @@ +# 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.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import ec2 + + +class EC2Tests(utils.TestCase): + + def test_create(self): + user_id = 'usr' + tenant_id = 'tnt' + req_body = { + "tenant_id": tenant_id, + } + resp_body = { + "credential": { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + } + } + self.stub_url('POST', ['users', user_id, 'credentials', + 'OS-EC2'], json=resp_body) + + cred = self.client.ec2.create(user_id, tenant_id) + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + self.assertRequestBodyIs(json=req_body) + + def test_get(self): + user_id = 'usr' + tenant_id = 'tnt' + resp_body = { + "credential": { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + } + } + self.stub_url('GET', ['users', user_id, 'credentials', + 'OS-EC2', 'access'], json=resp_body) + + cred = self.client.ec2.get(user_id, 'access') + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + + def test_list(self): + user_id = 'usr' + tenant_id = 'tnt' + resp_body = { + "credentials": { + "values": [ + { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + }, + { + "access": "another", + "secret": "key", + "tenant_id": tenant_id, + "created": "12/12/31", + "enabled": True, + } + ] + } + } + self.stub_url('GET', ['users', user_id, 'credentials', + 'OS-EC2'], json=resp_body) + + creds = self.client.ec2.list(user_id) + self.assertEqual(len(creds), 2) + cred = creds[0] + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + + def test_delete(self): + user_id = 'usr' + access = 'access' + self.stub_url('DELETE', ['users', user_id, 'credentials', + 'OS-EC2', access], status_code=204) + self.client.ec2.delete(user_id, access) diff --git a/keystoneclient/tests/unit/v2_0/test_endpoints.py b/keystoneclient/tests/unit/v2_0/test_endpoints.py new file mode 100644 index 0000000..ffc1c65 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_endpoints.py @@ -0,0 +1,147 @@ +# 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.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import endpoints + + +class EndpointTests(utils.TestCase): + def setUp(self): + super(EndpointTests, self).setUp() + self.TEST_ENDPOINTS = { + 'endpoints': [ + { + 'adminurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'id': '8f9531231e044e218824b0e58688d262', + 'internalurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'publicurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'region': 'RegionOne', + }, + { + 'adminurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'id': '8f9531231e044e218824b0e58688d263', + 'internalurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'publicurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'region': 'RegionOne', + } + ] + } + + def test_create_with_optional_params(self): + req_body = { + "endpoint": { + "region": "RegionOne", + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "service_id": uuid.uuid4().hex, + } + } + + resp_body = { + "endpoint": { + "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "region": "RegionOne", + "id": uuid.uuid4().hex, + "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + } + } + + self.stub_url('POST', ['endpoints'], json=resp_body) + + endpoint = self.client.endpoints.create( + region=req_body['endpoint']['region'], + publicurl=req_body['endpoint']['publicurl'], + adminurl=req_body['endpoint']['adminurl'], + internalurl=req_body['endpoint']['internalurl'], + service_id=req_body['endpoint']['service_id'] + ) + self.assertIsInstance(endpoint, endpoints.Endpoint) + self.assertRequestBodyIs(json=req_body) + + def test_create_with_optional_params_as_none(self): + req_body_without_defaults = { + "endpoint": { + "region": "RegionOne", + "service_id": uuid.uuid4().hex, + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": None, + "internalurl": None, + } + } + + resp_body = { + "endpoint": { + "region": "RegionOne", + "id": uuid.uuid4().hex, + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": None, + "internalurl": None, + } + } + + self.stub_url('POST', ['endpoints'], json=resp_body) + + endpoint_without_defaults = self.client.endpoints.create( + region=req_body_without_defaults['endpoint']['region'], + publicurl=req_body_without_defaults['endpoint']['publicurl'], + service_id=req_body_without_defaults['endpoint']['service_id'], + adminurl=None, + internalurl=None + ) + self.assertIsInstance(endpoint_without_defaults, endpoints.Endpoint) + self.assertRequestBodyIs(json=req_body_without_defaults) + + def test_create_without_optional_params(self): + req_body_without_defaults = { + "endpoint": { + "region": "RegionOne", + "service_id": uuid.uuid4().hex, + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": None, + "internalurl": None, + } + } + + resp_body = { + "endpoint": { + "region": "RegionOne", + "id": uuid.uuid4().hex, + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": None, + "internalurl": None, + } + } + + self.stub_url('POST', ['endpoints'], json=resp_body) + + endpoint_without_defaults = self.client.endpoints.create( + region=req_body_without_defaults['endpoint']['region'], + publicurl=req_body_without_defaults['endpoint']['publicurl'], + service_id=req_body_without_defaults['endpoint']['service_id'] + ) + self.assertIsInstance(endpoint_without_defaults, endpoints.Endpoint) + self.assertRequestBodyIs(json=req_body_without_defaults) + + def test_delete(self): + self.stub_url('DELETE', ['endpoints', '8f953'], status_code=204) + self.client.endpoints.delete('8f953') + + def test_list(self): + self.stub_url('GET', ['endpoints'], json=self.TEST_ENDPOINTS) + + endpoint_list = self.client.endpoints.list() + [self.assertIsInstance(r, endpoints.Endpoint) + for r in endpoint_list] diff --git a/keystoneclient/tests/unit/v2_0/test_extensions.py b/keystoneclient/tests/unit/v2_0/test_extensions.py new file mode 100644 index 0000000..d09943f --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_extensions.py @@ -0,0 +1,63 @@ +# 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.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import extensions + + +class ExtensionTests(utils.TestCase): + def setUp(self): + super(ExtensionTests, self).setUp() + self.TEST_EXTENSIONS = { + 'extensions': { + "values": [ + { + 'name': 'OpenStack Keystone User CRUD', + 'namespace': 'http://docs.openstack.org/' + 'identity/api/ext/OS-KSCRUD/v1.0', + 'updated': '2013-07-07T12:00:0-00:00', + 'alias': 'OS-KSCRUD', + 'description': + 'OpenStack extensions to Keystone v2.0 API' + ' enabling User Operations.', + 'links': + '[{"href":' + '"https://github.com/openstack/identity-api", "type":' + ' "text/html", "rel": "describedby"}]', + }, + { + 'name': 'OpenStack EC2 API', + 'namespace': 'http://docs.openstack.org/' + 'identity/api/ext/OS-EC2/v1.0', + 'updated': '2013-09-07T12:00:0-00:00', + 'alias': 'OS-EC2', + 'description': 'OpenStack EC2 Credentials backend.', + 'links': '[{"href":' + '"https://github.com/openstack/identity-api", "type":' + ' "text/html", "rel": "describedby"}]', + } + ] + } + } + + def test_list(self): + self.stub_url('GET', ['extensions'], json=self.TEST_EXTENSIONS) + extensions_list = self.client.extensions.list() + self.assertEqual(2, len(extensions_list)) + for extension in extensions_list: + self.assertIsInstance(extension, extensions.Extension) + self.assertIsNotNone(extension.alias) + self.assertIsNotNone(extension.description) + self.assertIsNotNone(extension.links) + self.assertIsNotNone(extension.name) + self.assertIsNotNone(extension.namespace) + self.assertIsNotNone(extension.updated) diff --git a/keystoneclient/tests/unit/v2_0/test_roles.py b/keystoneclient/tests/unit/v2_0/test_roles.py new file mode 100644 index 0000000..74ad1ed --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_roles.py @@ -0,0 +1,121 @@ +# 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.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import roles + + +class RoleTests(utils.TestCase): + def setUp(self): + super(RoleTests, self).setUp() + + self.ADMIN_ROLE_ID = uuid.uuid4().hex + self.MEMBER_ROLE_ID = uuid.uuid4().hex + + self.TEST_ROLES = { + "roles": { + "values": [ + { + "name": "admin", + "id": self.ADMIN_ROLE_ID, + }, + { + "name": "member", + "id": self.MEMBER_ROLE_ID, + } + ], + }, + } + + def test_create(self): + req_body = { + "role": { + "name": "sysadmin", + } + } + role_id = uuid.uuid4().hex + resp_body = { + "role": { + "name": "sysadmin", + "id": role_id, + } + } + self.stub_url('POST', ['OS-KSADM', 'roles'], json=resp_body) + + role = self.client.roles.create(req_body['role']['name']) + self.assertRequestBodyIs(json=req_body) + self.assertIsInstance(role, roles.Role) + self.assertEqual(role.id, role_id) + self.assertEqual(role.name, req_body['role']['name']) + + def test_delete(self): + self.stub_url('DELETE', + ['OS-KSADM', 'roles', self.ADMIN_ROLE_ID], + status_code=204) + self.client.roles.delete(self.ADMIN_ROLE_ID) + + def test_get(self): + self.stub_url('GET', ['OS-KSADM', 'roles', self.ADMIN_ROLE_ID], + json={'role': self.TEST_ROLES['roles']['values'][0]}) + + role = self.client.roles.get(self.ADMIN_ROLE_ID) + self.assertIsInstance(role, roles.Role) + self.assertEqual(role.id, self.ADMIN_ROLE_ID) + self.assertEqual(role.name, 'admin') + + def test_list(self): + self.stub_url('GET', ['OS-KSADM', 'roles'], + json=self.TEST_ROLES) + + role_list = self.client.roles.list() + [self.assertIsInstance(r, roles.Role) for r in role_list] + + def test_roles_for_user(self): + self.stub_url('GET', ['users', 'foo', 'roles'], + json=self.TEST_ROLES) + + role_list = self.client.roles.roles_for_user('foo') + [self.assertIsInstance(r, roles.Role) for r in role_list] + + def test_roles_for_user_tenant(self): + self.stub_url('GET', ['tenants', 'barrr', 'users', 'foo', + 'roles'], json=self.TEST_ROLES) + + role_list = self.client.roles.roles_for_user('foo', 'barrr') + [self.assertIsInstance(r, roles.Role) for r in role_list] + + def test_add_user_role(self): + self.stub_url('PUT', ['users', 'foo', 'roles', 'OS-KSADM', + 'barrr'], status_code=204) + + self.client.roles.add_user_role('foo', 'barrr') + + def test_add_user_role_tenant(self): + id_ = uuid.uuid4().hex + self.stub_url('PUT', ['tenants', id_, 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], status_code=204) + + self.client.roles.add_user_role('foo', 'barrr', id_) + + def test_remove_user_role(self): + self.stub_url('DELETE', ['users', 'foo', 'roles', 'OS-KSADM', + 'barrr'], status_code=204) + self.client.roles.remove_user_role('foo', 'barrr') + + def test_remove_user_role_tenant(self): + id_ = uuid.uuid4().hex + self.stub_url('DELETE', ['tenants', id_, 'users', 'foo', + 'roles', 'OS-KSADM', 'barrr'], + status_code=204) + self.client.roles.remove_user_role('foo', 'barrr', id_) diff --git a/keystoneclient/tests/unit/v2_0/test_service_catalog.py b/keystoneclient/tests/unit/v2_0/test_service_catalog.py new file mode 100644 index 0000000..e9ebf50 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_service_catalog.py @@ -0,0 +1,175 @@ +# 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 import access +from keystoneclient import exceptions +from keystoneclient.tests.unit.v2_0 import client_fixtures +from keystoneclient.tests.unit.v2_0 import utils + + +class ServiceCatalogTest(utils.TestCase): + def setUp(self): + super(ServiceCatalogTest, self).setUp() + self.AUTH_RESPONSE_BODY = client_fixtures.auth_response_body() + + def test_building_a_service_catalog(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + self.assertEqual(sc.url_for(service_type='compute'), + "https://compute.north.host/v1/1234") + self.assertEqual(sc.url_for('tenantId', '1', service_type='compute'), + "https://compute.north.host/v1/1234") + self.assertEqual(sc.url_for('tenantId', '2', service_type='compute'), + "https://compute.north.host/v1.1/3456") + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", + "South", service_type='compute') + + def test_service_catalog_endpoints(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + public_ep = sc.get_endpoints(service_type='compute', + endpoint_type='publicURL') + self.assertEqual(public_ep['compute'][1]['tenantId'], '2') + self.assertEqual(public_ep['compute'][1]['versionId'], '1.1') + self.assertEqual(public_ep['compute'][1]['internalURL'], + "https://compute.north.host/v1.1/3456") + + def test_service_catalog_regions(self): + self.AUTH_RESPONSE_BODY['access']['region_name'] = "North" + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='publicURL') + self.assertEqual(url, "https://image.north.host/v1/") + + self.AUTH_RESPONSE_BODY['access']['region_name'] = "South" + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='internalURL') + self.assertEqual(url, "https://image-internal.south.host/v1/") + + def test_service_catalog_empty(self): + self.AUTH_RESPONSE_BODY['access']['serviceCatalog'] = [] + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + self.assertRaises(exceptions.EmptyCatalog, + auth_ref.service_catalog.url_for, + service_type='image', + endpoint_type='internalURL') + + def test_service_catalog_get_endpoints_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + endpoints = sc.get_endpoints(service_type='image', region_name='North') + self.assertEqual(len(endpoints), 1) + self.assertEqual(endpoints['image'][0]['publicURL'], + 'https://image.north.host/v1/') + + endpoints = sc.get_endpoints(service_type='image', region_name='South') + self.assertEqual(len(endpoints), 1) + self.assertEqual(endpoints['image'][0]['publicURL'], + 'https://image.south.host/v1/') + + endpoints = sc.get_endpoints(service_type='compute') + self.assertEqual(len(endpoints['compute']), 2) + + endpoints = sc.get_endpoints(service_type='compute', + region_name='North') + self.assertEqual(len(endpoints['compute']), 2) + + endpoints = sc.get_endpoints(service_type='compute', + region_name='West') + self.assertEqual(len(endpoints['compute']), 0) + + def test_service_catalog_url_for_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', region_name='North') + self.assertEqual(url, 'https://image.north.host/v1/') + + url = sc.url_for(service_type='image', region_name='South') + self.assertEqual(url, 'https://image.south.host/v1/') + + url = sc.url_for(service_type='compute', + region_name='North', + attr='versionId', + filter_value='1.1') + self.assertEqual(url, 'https://compute.north.host/v1.1/3456') + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + service_type='image', region_name='West') + + def test_servcie_catalog_get_url_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + urls = sc.get_urls(service_type='image') + self.assertEqual(len(urls), 2) + + urls = sc.get_urls(service_type='image', region_name='North') + self.assertEqual(len(urls), 1) + self.assertEqual(urls[0], 'https://image.north.host/v1/') + + urls = sc.get_urls(service_type='image', region_name='South') + self.assertEqual(len(urls), 1) + self.assertEqual(urls[0], 'https://image.south.host/v1/') + + urls = sc.get_urls(service_type='image', region_name='West') + self.assertIsNone(urls) + + def test_service_catalog_param_overrides_body_region(self): + self.AUTH_RESPONSE_BODY['access']['region_name'] = "North" + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image') + self.assertEqual(url, 'https://image.north.host/v1/') + + url = sc.url_for(service_type='image', region_name='South') + self.assertEqual(url, 'https://image.south.host/v1/') + + endpoints = sc.get_endpoints(service_type='image') + self.assertEqual(len(endpoints['image']), 1) + self.assertEqual(endpoints['image'][0]['publicURL'], + 'https://image.north.host/v1/') + + endpoints = sc.get_endpoints(service_type='image', region_name='South') + self.assertEqual(len(endpoints['image']), 1) + self.assertEqual(endpoints['image'][0]['publicURL'], + 'https://image.south.host/v1/') + + def test_service_catalog_service_name(self): + auth_ref = access.AccessInfo.factory(resp=None, + body=self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_name='Image Servers', endpoint_type='public', + service_type='image', region_name='North') + self.assertEqual('https://image.north.host/v1/', url) + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + service_name='Image Servers', service_type='compute') + + urls = sc.get_urls(service_type='image', service_name='Image Servers', + endpoint_type='public') + + self.assertIn('https://image.north.host/v1/', urls) + self.assertIn('https://image.south.host/v1/', urls) + + urls = sc.get_urls(service_type='image', service_name='Servers', + endpoint_type='public') + + self.assertIsNone(urls) diff --git a/keystoneclient/tests/unit/v2_0/test_services.py b/keystoneclient/tests/unit/v2_0/test_services.py new file mode 100644 index 0000000..2172745 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_services.py @@ -0,0 +1,98 @@ +# 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.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import services + + +class ServiceTests(utils.TestCase): + def setUp(self): + super(ServiceTests, self).setUp() + + self.NOVA_SERVICE_ID = uuid.uuid4().hex + self.KEYSTONE_SERVICE_ID = uuid.uuid4().hex + + self.TEST_SERVICES = { + "OS-KSADM:services": { + "values": [ + { + "name": "nova", + "type": "compute", + "description": "Nova-compatible service.", + "id": self.NOVA_SERVICE_ID + }, + { + "name": "keystone", + "type": "identity", + "description": "Keystone-compatible service.", + "id": self.KEYSTONE_SERVICE_ID + }, + ], + }, + } + + def test_create(self): + req_body = { + "OS-KSADM:service": { + "name": "swift", + "type": "object-store", + "description": "Swift-compatible service.", + } + } + service_id = uuid.uuid4().hex + resp_body = { + "OS-KSADM:service": { + "name": "swift", + "type": "object-store", + "description": "Swift-compatible service.", + "id": service_id, + } + } + self.stub_url('POST', ['OS-KSADM', 'services'], json=resp_body) + + service = self.client.services.create( + req_body['OS-KSADM:service']['name'], + req_body['OS-KSADM:service']['type'], + req_body['OS-KSADM:service']['description']) + self.assertIsInstance(service, services.Service) + self.assertEqual(service.id, service_id) + self.assertEqual(service.name, req_body['OS-KSADM:service']['name']) + self.assertRequestBodyIs(json=req_body) + + def test_delete(self): + self.stub_url('DELETE', + ['OS-KSADM', 'services', self.NOVA_SERVICE_ID], + status_code=204) + + self.client.services.delete(self.NOVA_SERVICE_ID) + + def test_get(self): + test_services = self.TEST_SERVICES['OS-KSADM:services']['values'][0] + + self.stub_url('GET', ['OS-KSADM', 'services', self.NOVA_SERVICE_ID], + json={'OS-KSADM:service': test_services}) + + service = self.client.services.get(self.NOVA_SERVICE_ID) + self.assertIsInstance(service, services.Service) + self.assertEqual(service.id, self.NOVA_SERVICE_ID) + self.assertEqual(service.name, 'nova') + self.assertEqual(service.type, 'compute') + + def test_list(self): + self.stub_url('GET', ['OS-KSADM', 'services'], + json=self.TEST_SERVICES) + + service_list = self.client.services.list() + [self.assertIsInstance(r, services.Service) + for r in service_list] diff --git a/keystoneclient/tests/unit/v2_0/test_shell.py b/keystoneclient/tests/unit/v2_0/test_shell.py new file mode 100644 index 0000000..be91d23 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_shell.py @@ -0,0 +1,464 @@ +# 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 os +import sys + +import mock +from oslo_serialization import jsonutils +import six +from testtools import matchers + +from keystoneclient import fixture +from keystoneclient.tests.unit.v2_0 import utils + + +DEFAULT_USERNAME = 'username' +DEFAULT_PASSWORD = 'password' +DEFAULT_TENANT_ID = 'tenant_id' +DEFAULT_TENANT_NAME = 'tenant_name' +DEFAULT_AUTH_URL = 'http://127.0.0.1:5000/v2.0/' +DEFAULT_ADMIN_URL = 'http://127.0.0.1:35357/v2.0/' + + +class ShellTests(utils.TestCase): + + TEST_URL = DEFAULT_ADMIN_URL + + def setUp(self): + """Patch os.environ to avoid required auth info.""" + + super(ShellTests, self).setUp() + + self.old_environment = os.environ.copy() + os.environ = { + 'OS_USERNAME': DEFAULT_USERNAME, + 'OS_PASSWORD': DEFAULT_PASSWORD, + 'OS_TENANT_ID': DEFAULT_TENANT_ID, + 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, + 'OS_AUTH_URL': DEFAULT_AUTH_URL, + } + import keystoneclient.shell + self.shell = keystoneclient.shell.OpenStackIdentityShell() + + self.token = fixture.V2Token() + self.token.set_scope() + svc = self.token.add_service('identity') + svc.add_endpoint(public=DEFAULT_AUTH_URL, + admin=DEFAULT_ADMIN_URL) + + self.stub_auth(json=self.token, base_url=DEFAULT_AUTH_URL) + + def tearDown(self): + os.environ = self.old_environment + super(ShellTests, self).tearDown() + + def run_command(self, cmd): + orig = sys.stdout + try: + sys.stdout = six.StringIO() + if isinstance(cmd, list): + self.shell.main(cmd) + else: + self.shell.main(cmd.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertEqual(exc_value.code, 0) + finally: + out = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + return out + + def assert_called(self, method, path, base_url=TEST_URL): + self.assertEqual(method, self.requests.last_request.method) + self.assertEqual(base_url + path.lstrip('/'), + self.requests.last_request.url) + + def test_user_list(self): + self.stub_url('GET', ['users'], json={'users': []}) + self.run_command('user-list') + self.assert_called('GET', '/users') + + def test_user_create(self): + self.stub_url('POST', ['users'], json={'user': {}}) + self.run_command('user-create --name new-user') + + self.assert_called('POST', '/users') + self.assertRequestBodyIs(json={'user': {'email': None, + 'password': None, + 'enabled': True, + 'name': 'new-user', + 'tenantId': None}}) + + @mock.patch('sys.stdin', autospec=True) + def test_user_create_password_prompt(self, mock_stdin): + self.stub_url('POST', ['users'], json={'user': {}}) + + with mock.patch('getpass.getpass') as mock_getpass: + del(os.environ['OS_PASSWORD']) + mock_stdin.isatty = lambda: True + mock_getpass.return_value = 'newpass' + self.run_command('user-create --name new-user --pass') + + self.assert_called('POST', '/users') + self.assertRequestBodyIs(json={'user': {'email': None, + 'password': 'newpass', + 'enabled': True, + 'name': 'new-user', + 'tenantId': None}}) + + def test_user_get(self): + self.stub_url('GET', ['users', '1'], + json={'user': {'id': '1'}}) + self.run_command('user-get 1') + self.assert_called('GET', '/users/1') + + def test_user_delete(self): + self.stub_url('GET', ['users', '1'], + json={'user': {'id': '1'}}) + self.stub_url('DELETE', ['users', '1']) + self.run_command('user-delete 1') + self.assert_called('DELETE', '/users/1') + + def test_user_password_update(self): + self.stub_url('GET', ['users', '1'], + json={'user': {'id': '1'}}) + self.stub_url('PUT', ['users', '1', 'OS-KSADM', 'password']) + self.run_command('user-password-update --pass newpass 1') + self.assert_called('PUT', '/users/1/OS-KSADM/password') + + def test_user_update(self): + self.stub_url('PUT', ['users', '1']) + self.stub_url('GET', ['users', '1'], + json={"user": {"tenantId": "1", + "enabled": "true", + "id": "1", + "name": "username"}}) + + self.run_command('user-update --name new-user1' + ' --email user@email.com --enabled true 1') + self.assert_called('PUT', '/users/1') + body = {'user': {'id': '1', 'email': 'user@email.com', + 'enabled': True, 'name': 'new-user1'}} + self.assertRequestBodyIs(json=body) + + required = 'User not updated, no arguments present.' + out = self.run_command('user-update 1') + self.assertThat(out, matchers.MatchesRegex(required)) + + self.run_command(['user-update', '--email', '', '1']) + self.assert_called('PUT', '/users/1') + self.assertRequestBodyIs(json={'user': {'id': '1', 'email': ''}}) + + def test_role_create(self): + self.stub_url('POST', ['OS-KSADM', 'roles'], json={'role': {}}) + self.run_command('role-create --name new-role') + self.assert_called('POST', '/OS-KSADM/roles') + self.assertRequestBodyIs(json={"role": {"name": "new-role"}}) + + def test_role_get(self): + self.stub_url('GET', ['OS-KSADM', 'roles', '1'], + json={'role': {'id': '1'}}) + self.run_command('role-get 1') + self.assert_called('GET', '/OS-KSADM/roles/1') + + def test_role_list(self): + self.stub_url('GET', ['OS-KSADM', 'roles'], json={'roles': []}) + self.run_command('role-list') + self.assert_called('GET', '/OS-KSADM/roles') + + def test_role_delete(self): + self.stub_url('GET', ['OS-KSADM', 'roles', '1'], + json={'role': {'id': '1'}}) + self.stub_url('DELETE', ['OS-KSADM', 'roles', '1']) + self.run_command('role-delete 1') + self.assert_called('DELETE', '/OS-KSADM/roles/1') + + def test_user_role_add(self): + self.stub_url('GET', ['users', '1'], + json={'user': {'id': '1'}}) + self.stub_url('GET', ['OS-KSADM', 'roles', '1'], + json={'role': {'id': '1'}}) + + self.stub_url('PUT', ['users', '1', 'roles', 'OS-KSADM', '1']) + self.run_command('user-role-add --user_id 1 --role_id 1') + self.assert_called('PUT', '/users/1/roles/OS-KSADM/1') + + def test_user_role_list(self): + self.stub_url('GET', ['tenants', self.token.tenant_id], + json={'tenant': {'id': self.token.tenant_id}}) + self.stub_url('GET', ['tenants', self.token.tenant_id, + 'users', self.token.user_id, 'roles'], + json={'roles': []}) + + url = '/tenants/%s/users/%s/roles' % (self.token.tenant_id, + self.token.user_id) + + self.run_command('user-role-list --user_id %s --tenant-id %s' % + (self.token.user_id, self.token.tenant_id)) + self.assert_called('GET', url) + + self.run_command('user-role-list --user_id %s' % self.token.user_id) + self.assert_called('GET', url) + + self.run_command('user-role-list') + self.assert_called('GET', url) + + def test_user_role_remove(self): + self.stub_url('GET', ['users', '1'], + json={'user': {'id': 1}}) + self.stub_url('GET', ['OS-KSADM', 'roles', '1'], + json={'role': {'id': 1}}) + self.stub_url('DELETE', + ['users', '1', 'roles', 'OS-KSADM', '1']) + + self.run_command('user-role-remove --user_id 1 --role_id 1') + self.assert_called('DELETE', '/users/1/roles/OS-KSADM/1') + + def test_tenant_create(self): + self.stub_url('POST', ['tenants'], json={'tenant': {}}) + self.run_command('tenant-create --name new-tenant') + self.assertRequestBodyIs(json={"tenant": {"enabled": True, + "name": "new-tenant", + "description": None}}) + + def test_tenant_get(self): + self.stub_url('GET', ['tenants', '2'], json={'tenant': {}}) + self.run_command('tenant-get 2') + self.assert_called('GET', '/tenants/2') + + def test_tenant_list(self): + self.stub_url('GET', ['tenants'], json={'tenants': []}) + self.run_command('tenant-list') + self.assert_called('GET', '/tenants') + + def test_tenant_update(self): + self.stub_url('GET', ['tenants', '1'], + json={'tenant': {'id': '1'}}) + self.stub_url('GET', ['tenants', '2'], + json={'tenant': {'id': '2'}}) + self.stub_url('POST', ['tenants', '2'], + json={'tenant': {'id': '2'}}) + self.run_command('tenant-update' + ' --name new-tenant1 --enabled false' + ' --description desc 2') + self.assert_called('POST', '/tenants/2') + self.assertRequestBodyIs(json={"tenant": {"enabled": False, + "id": "2", + "description": "desc", + "name": "new-tenant1"}}) + + required = 'Tenant not updated, no arguments present.' + out = self.run_command('tenant-update 1') + self.assertThat(out, matchers.MatchesRegex(required)) + + def test_tenant_delete(self): + self.stub_url('GET', ['tenants', '2'], + json={'tenant': {'id': '2'}}) + self.stub_url('DELETE', ['tenants', '2']) + self.run_command('tenant-delete 2') + self.assert_called('DELETE', '/tenants/2') + + def test_service_create_with_required_arguments_only(self): + self.stub_url('POST', ['OS-KSADM', 'services'], + json={'OS-KSADM:service': {}}) + self.run_command('service-create --type compute') + self.assert_called('POST', '/OS-KSADM/services') + json = {"OS-KSADM:service": {"type": "compute", + "name": None, + "description": None}} + self.assertRequestBodyIs(json=json) + + def test_service_create_with_all_arguments(self): + self.stub_url('POST', ['OS-KSADM', 'services'], + json={'OS-KSADM:service': {}}) + self.run_command('service-create --type compute ' + '--name service1 --description desc1') + self.assert_called('POST', '/OS-KSADM/services') + json = {"OS-KSADM:service": {"type": "compute", + "name": "service1", + "description": "desc1"}} + self.assertRequestBodyIs(json=json) + + def test_service_get(self): + self.stub_url('GET', ['OS-KSADM', 'services', '1'], + json={'OS-KSADM:service': {'id': '1'}}) + self.run_command('service-get 1') + self.assert_called('GET', '/OS-KSADM/services/1') + + def test_service_list(self): + self.stub_url('GET', ['OS-KSADM', 'services'], + json={'OS-KSADM:services': []}) + self.run_command('service-list') + self.assert_called('GET', '/OS-KSADM/services') + + def test_service_delete(self): + self.stub_url('GET', ['OS-KSADM', 'services', '1'], + json={'OS-KSADM:service': {'id': 1}}) + self.stub_url('DELETE', ['OS-KSADM', 'services', '1']) + self.run_command('service-delete 1') + self.assert_called('DELETE', '/OS-KSADM/services/1') + + def test_catalog(self): + self.run_command('catalog') + self.run_command('catalog --service compute') + + def test_ec2_credentials_create(self): + self.stub_url('POST', + ['users', self.token.user_id, 'credentials', 'OS-EC2'], + json={'credential': {}}) + + url = '/users/%s/credentials/OS-EC2' % self.token.user_id + self.run_command('ec2-credentials-create --tenant-id 1 ' + '--user-id %s' % self.token.user_id) + self.assert_called('POST', url) + self.assertRequestBodyIs(json={'tenant_id': '1'}) + + self.run_command('ec2-credentials-create --tenant-id 1') + self.assert_called('POST', url) + self.assertRequestBodyIs(json={'tenant_id': '1'}) + + self.run_command('ec2-credentials-create') + self.assert_called('POST', url) + self.assertRequestBodyIs(json={'tenant_id': self.token.tenant_id}) + + def test_ec2_credentials_delete(self): + self.stub_url('DELETE', + ['users', self.token.user_id, + 'credentials', 'OS-EC2', '2']) + self.run_command('ec2-credentials-delete --access 2 --user-id %s' % + self.token.user_id) + + url = '/users/%s/credentials/OS-EC2/2' % self.token.user_id + self.assert_called('DELETE', url) + + self.run_command('ec2-credentials-delete --access 2') + self.assert_called('DELETE', url) + + def test_ec2_credentials_list(self): + self.stub_url('GET', + ['users', self.token.user_id, 'credentials', 'OS-EC2'], + json={'credentials': []}) + self.run_command('ec2-credentials-list --user-id %s' + % self.token.user_id) + + url = '/users/%s/credentials/OS-EC2' % self.token.user_id + self.assert_called('GET', url) + + self.run_command('ec2-credentials-list') + self.assert_called('GET', url) + + def test_ec2_credentials_get(self): + self.stub_url('GET', + ['users', '1', 'credentials', 'OS-EC2', '2'], + json={'credential': {}}) + self.run_command('ec2-credentials-get --access 2 --user-id 1') + self.assert_called('GET', '/users/1/credentials/OS-EC2/2') + + def test_bootstrap(self): + user = {'user': {'id': '1'}} + role = {'role': {'id': '1'}} + tenant = {'tenant': {'id': '1'}} + + token = fixture.V2Token(user_id=1, tenant_id=1) + token.add_role(id=1) + svc = token.add_service('identity') + svc.add_endpoint(public=DEFAULT_AUTH_URL, + admin=DEFAULT_ADMIN_URL) + + self.stub_auth(json=token) + + self.stub_url('POST', ['OS-KSADM', 'roles'], json=role) + self.stub_url('GET', ['OS-KSADM', 'roles', '1'], json=role) + self.stub_url('POST', ['tenants'], json=tenant) + self.stub_url('GET', ['tenants', '1'], json=tenant) + self.stub_url('POST', ['users'], json=user) + self.stub_url('GET', ['users', '1'], json=user) + self.stub_url('PUT', + ['tenants', '1', 'users', '1', 'roles', 'OS-KSADM', '1'], + json=role) + + self.run_command('bootstrap --user-name new-user' + ' --pass 1 --role-name admin' + ' --tenant-name new-tenant') + + def called_anytime(method, path, json=None): + test_url = self.TEST_URL.strip('/') + for r in self.requests.request_history: + if not r.method == method: + continue + if not r.url == test_url + path: + continue + + if json: + json_body = jsonutils.loads(r.body) + if not json_body == json: + continue + + return True + + raise AssertionError('URL never called') + + called_anytime('POST', '/users', {'user': {'email': None, + 'password': '1', + 'enabled': True, + 'name': 'new-user', + 'tenantId': None}}) + + called_anytime('POST', '/tenants', {"tenant": {"enabled": True, + "name": "new-tenant", + "description": None}}) + + called_anytime('POST', '/OS-KSADM/roles', + {"role": {"name": "admin"}}) + + called_anytime('PUT', '/tenants/1/users/1/roles/OS-KSADM/1') + + def test_bash_completion(self): + self.run_command('bash-completion') + + def test_help(self): + out = self.run_command('help') + required = 'usage: keystone' + self.assertThat(out, matchers.MatchesRegex(required)) + + def test_password_update(self): + self.stub_url('PATCH', + ['OS-KSCRUD', 'users', self.token.user_id], + base_url=DEFAULT_AUTH_URL) + self.run_command('password-update --current-password oldpass' + ' --new-password newpass') + self.assert_called('PATCH', + '/OS-KSCRUD/users/%s' % self.token.user_id, + base_url=DEFAULT_AUTH_URL) + self.assertRequestBodyIs(json={'user': {'original_password': 'oldpass', + 'password': 'newpass'}}) + + def test_endpoint_create(self): + self.stub_url('GET', ['OS-KSADM', 'services', '1'], + json={'OS-KSADM:service': {'id': '1'}}) + self.stub_url('POST', ['endpoints'], json={'endpoint': {}}) + self.run_command('endpoint-create --service-id 1 ' + '--publicurl=http://example.com:1234/go') + self.assert_called('POST', '/endpoints') + json = {'endpoint': {'adminurl': None, + 'service_id': '1', + 'region': 'regionOne', + 'internalurl': None, + 'publicurl': "http://example.com:1234/go"}} + self.assertRequestBodyIs(json=json) + + def test_endpoint_list(self): + self.stub_url('GET', ['endpoints'], json={'endpoints': []}) + self.run_command('endpoint-list') + self.assert_called('GET', '/endpoints') diff --git a/keystoneclient/tests/unit/v2_0/test_tenants.py b/keystoneclient/tests/unit/v2_0/test_tenants.py new file mode 100644 index 0000000..62ca398 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_tenants.py @@ -0,0 +1,363 @@ +# 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 import exceptions +from keystoneclient import fixture +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import client +from keystoneclient.v2_0 import tenants +from keystoneclient.v2_0 import users + + +class TenantTests(utils.TestCase): + def setUp(self): + super(TenantTests, self).setUp() + + self.INVIS_ID = uuid.uuid4().hex + self.DEMO_ID = uuid.uuid4().hex + self.ADMIN_ID = uuid.uuid4().hex + self.EXTRAS_ID = uuid.uuid4().hex + + self.TEST_TENANTS = { + "tenants": { + "values": [ + { + "enabled": True, + "description": "A description change!", + "name": "invisible_to_admin", + "id": self.INVIS_ID, + }, + { + "enabled": True, + "description": "None", + "name": "demo", + "id": self.DEMO_ID, + }, + { + "enabled": True, + "description": "None", + "name": "admin", + "id": self.ADMIN_ID, + }, + { + "extravalue01": "metadata01", + "enabled": True, + "description": "For testing extras", + "name": "test_extras", + "id": self.EXTRAS_ID, + } + ], + "links": [], + }, + } + + def test_create(self): + req_body = { + "tenant": { + "name": "tenantX", + "description": "Like tenant 9, but better.", + "enabled": True, + "extravalue01": "metadata01", + }, + } + id_ = uuid.uuid4().hex + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": True, + "id": id_, + "description": "Like tenant 9, but better.", + "extravalue01": "metadata01", + } + } + self.stub_url('POST', ['tenants'], json=resp_body) + + tenant = self.client.tenants.create( + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled'], + extravalue01=req_body['tenant']['extravalue01'], + name="don't overwrite priors") + self.assertIsInstance(tenant, tenants.Tenant) + self.assertEqual(tenant.id, id_) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "Like tenant 9, but better.") + self.assertEqual(tenant.extravalue01, "metadata01") + self.assertRequestBodyIs(json=req_body) + + def test_duplicate_create(self): + req_body = { + "tenant": { + "name": "tenantX", + "description": "The duplicate tenant.", + "enabled": True + }, + } + resp_body = { + "error": { + "message": "Conflict occurred attempting to store project.", + "code": 409, + "title": "Conflict", + } + } + self.stub_url('POST', ['tenants'], status_code=409, json=resp_body) + + def create_duplicate_tenant(): + self.client.tenants.create(req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled']) + + self.assertRaises(exceptions.Conflict, create_duplicate_tenant) + + def test_delete(self): + self.stub_url('DELETE', ['tenants', self.ADMIN_ID], status_code=204) + self.client.tenants.delete(self.ADMIN_ID) + + def test_get(self): + resp = {'tenant': self.TEST_TENANTS['tenants']['values'][2]} + self.stub_url('GET', ['tenants', self.ADMIN_ID], json=resp) + + t = self.client.tenants.get(self.ADMIN_ID) + self.assertIsInstance(t, tenants.Tenant) + self.assertEqual(t.id, self.ADMIN_ID) + self.assertEqual(t.name, 'admin') + + def test_list(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list() + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + def test_list_limit(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(limit=1) + self.assertQueryStringIs('limit=1') + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + def test_list_marker(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(marker=1) + self.assertQueryStringIs('marker=1') + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + def test_list_limit_marker(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(limit=1, marker=1) + self.assertQueryStringIs('marker=1&limit=1') + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + def test_update(self): + req_body = { + "tenant": { + "id": self.EXTRAS_ID, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + "extravalue01": "metadataChanged", + # "extraname": "dontoverwrite!", + }, + } + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": False, + "id": self.EXTRAS_ID, + "description": "I changed you!", + "extravalue01": "metadataChanged", + }, + } + + self.stub_url('POST', ['tenants', self.EXTRAS_ID], json=resp_body) + + tenant = self.client.tenants.update( + req_body['tenant']['id'], + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled'], + extravalue01=req_body['tenant']['extravalue01'], + name="don't overwrite priors") + self.assertIsInstance(tenant, tenants.Tenant) + self.assertRequestBodyIs(json=req_body) + self.assertEqual(tenant.id, self.EXTRAS_ID) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "I changed you!") + self.assertFalse(tenant.enabled) + self.assertEqual(tenant.extravalue01, "metadataChanged") + + def test_update_empty_description(self): + req_body = { + "tenant": { + "id": self.EXTRAS_ID, + "name": "tenantX", + "description": "", + "enabled": False, + }, + } + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": False, + "id": self.EXTRAS_ID, + "description": "", + }, + } + self.stub_url('POST', ['tenants', self.EXTRAS_ID], json=resp_body) + + tenant = self.client.tenants.update(req_body['tenant']['id'], + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled']) + self.assertIsInstance(tenant, tenants.Tenant) + self.assertRequestBodyIs(json=req_body) + self.assertEqual(tenant.id, self.EXTRAS_ID) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "") + self.assertFalse(tenant.enabled) + + def test_add_user(self): + self.stub_url('PUT', + ['tenants', self.EXTRAS_ID, 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], + status_code=204) + + self.client.tenants.add_user(self.EXTRAS_ID, 'foo', 'barrr') + + def test_remove_user(self): + self.stub_url('DELETE', ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status_code=204) + + self.client.tenants.remove_user(self.EXTRAS_ID, 'foo', 'barrr') + + def test_tenant_add_user(self): + self.stub_url('PUT', ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status_code=204) + + req_body = { + "tenant": { + "id": self.EXTRAS_ID, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + }, + } + # make tenant object with manager + tenant = self.client.tenants.resource_class(self.client.tenants, + req_body['tenant']) + tenant.add_user('foo', 'barrr') + self.assertIsInstance(tenant, tenants.Tenant) + + def test_tenant_remove_user(self): + self.stub_url('DELETE', ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status_code=204) + + req_body = { + "tenant": { + "id": self.EXTRAS_ID, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + }, + } + + # make tenant object with manager + tenant = self.client.tenants.resource_class(self.client.tenants, + req_body['tenant']) + tenant.remove_user('foo', 'barrr') + self.assertIsInstance(tenant, tenants.Tenant) + + def test_tenant_list_users(self): + tenant_id = uuid.uuid4().hex + user_id1 = uuid.uuid4().hex + user_id2 = uuid.uuid4().hex + + tenant_resp = { + 'tenant': { + 'name': uuid.uuid4().hex, + 'enabled': True, + 'id': tenant_id, + 'description': 'test tenant', + } + } + + users_resp = { + 'users': { + 'values': [ + { + 'email': uuid.uuid4().hex, + 'enabled': True, + 'id': user_id1, + 'name': uuid.uuid4().hex, + }, + { + 'email': uuid.uuid4().hex, + 'enabled': True, + 'id': user_id2, + 'name': uuid.uuid4().hex, + }, + ] + } + } + + self.stub_url('GET', ['tenants', tenant_id], json=tenant_resp) + self.stub_url('GET', + ['tenants', tenant_id, 'users'], + json=users_resp) + + tenant = self.client.tenants.get(tenant_id) + user_objs = tenant.list_users() + + for u in user_objs: + self.assertIsInstance(u, users.User) + + self.assertEqual(set([user_id1, user_id2]), + set([u.id for u in user_objs])) + + def test_list_tenants_use_admin_url(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + self.assertEqual(self.TEST_URL, self.client.management_url) + + tenant_list = self.client.tenants.list() + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + self.assertEqual(len(self.TEST_TENANTS['tenants']['values']), + len(tenant_list)) + + def test_list_tenants_fallback_to_auth_url(self): + new_auth_url = 'http://keystone.test:5000/v2.0' + + token = fixture.V2Token(token_id=self.TEST_TOKEN, + user_name=self.TEST_USER, + user_id=self.TEST_USER_ID) + + self.stub_auth(base_url=new_auth_url, json=token) + self.stub_url('GET', ['tenants'], base_url=new_auth_url, + json=self.TEST_TENANTS) + + c = client.Client(username=self.TEST_USER, + auth_url=new_auth_url, + password=uuid.uuid4().hex) + + self.assertIsNone(c.management_url) + tenant_list = c.tenants.list() + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + self.assertEqual(len(self.TEST_TENANTS['tenants']['values']), + len(tenant_list)) diff --git a/keystoneclient/tests/unit/v2_0/test_tokens.py b/keystoneclient/tests/unit/v2_0/test_tokens.py new file mode 100644 index 0000000..8a40f82 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_tokens.py @@ -0,0 +1,208 @@ +# 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 import access +from keystoneclient import exceptions +from keystoneclient import fixture +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import client +from keystoneclient.v2_0 import tokens + + +class TokenTests(utils.TestCase): + + def test_delete(self): + id_ = uuid.uuid4().hex + self.stub_url('DELETE', ['tokens', id_], status_code=204) + self.client.tokens.delete(id_) + + def test_user_password(self): + token_fixture = fixture.V2Token(user_name=self.TEST_USER) + self.stub_auth(json=token_fixture) + + password = uuid.uuid4().hex + token_ref = self.client.tokens.authenticate(username=self.TEST_USER, + password=password) + + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + req_body = { + 'auth': { + 'passwordCredentials': { + 'username': self.TEST_USER, + 'password': password, + } + } + } + + self.assertRequestBodyIs(json=req_body) + + def test_with_token_id(self): + token_fixture = fixture.V2Token() + self.stub_auth(json=token_fixture) + + token_id = uuid.uuid4().hex + token_ref = self.client.tokens.authenticate(token=token_id) + + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + req_body = { + 'auth': { + 'token': { + 'id': token_id, + } + } + } + + self.assertRequestBodyIs(json=req_body) + + def test_without_auth_params(self): + self.assertRaises(ValueError, self.client.tokens.authenticate) + self.assertRaises(ValueError, self.client.tokens.authenticate, + tenant_id=uuid.uuid4().hex) + + def test_with_tenant_id(self): + token_fixture = fixture.V2Token() + token_fixture.set_scope() + self.stub_auth(json=token_fixture) + + token_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + token_ref = self.client.tokens.authenticate(token=token_id, + tenant_id=tenant_id) + + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + tenant_data = {'id': token_fixture.tenant_id, + 'name': token_fixture.tenant_name} + self.assertEqual(tenant_data, token_ref.tenant) + + req_body = { + 'auth': { + 'token': { + 'id': token_id, + }, + 'tenantId': tenant_id + } + } + + self.assertRequestBodyIs(json=req_body) + + def test_with_tenant_name(self): + token_fixture = fixture.V2Token() + token_fixture.set_scope() + self.stub_auth(json=token_fixture) + + token_id = uuid.uuid4().hex + tenant_name = uuid.uuid4().hex + token_ref = self.client.tokens.authenticate(token=token_id, + tenant_name=tenant_name) + + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + tenant_data = {'id': token_fixture.tenant_id, + 'name': token_fixture.tenant_name} + self.assertEqual(tenant_data, token_ref.tenant) + + req_body = { + 'auth': { + 'token': { + 'id': token_id, + }, + 'tenantName': tenant_name + } + } + + self.assertRequestBodyIs(json=req_body) + + def test_authenticate_use_admin_url(self): + token_fixture = fixture.V2Token() + token_fixture.set_scope() + self.stub_auth(json=token_fixture) + + self.assertEqual(self.TEST_URL, self.client.management_url) + + token_ref = self.client.tokens.authenticate(token=uuid.uuid4().hex) + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + def test_authenticate_fallback_to_auth_url(self): + new_auth_url = 'http://keystone.test:5000/v2.0' + + token_fixture = fixture.V2Token() + self.stub_auth(base_url=new_auth_url, json=token_fixture) + + c = client.Client(username=self.TEST_USER, + auth_url=new_auth_url, + password=uuid.uuid4().hex) + + self.assertIsNone(c.management_url) + + token_ref = c.tokens.authenticate(token=uuid.uuid4().hex) + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + def test_validate_token(self): + id_ = uuid.uuid4().hex + token_fixture = fixture.V2Token(token_id=id_) + self.stub_url('GET', ['tokens', id_], json=token_fixture) + + token_ref = self.client.tokens.validate(id_) + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(id_, token_ref.id) + + def test_validate_token_invalid_token(self): + # If the token is invalid, typically a NotFound is raised. + + id_ = uuid.uuid4().hex + # The server is expected to return 404 if the token is invalid. + self.stub_url('GET', ['tokens', id_], status_code=404) + self.assertRaises(exceptions.NotFound, + self.client.tokens.validate, id_) + + def test_validate_token_access_info_with_token_id(self): + # Can validate a token passing a string token ID. + token_id = uuid.uuid4().hex + token_fixture = fixture.V2Token(token_id=token_id) + self.stub_url('GET', ['tokens', token_id], json=token_fixture) + access_info = self.client.tokens.validate_access_info(token_id) + self.assertIsInstance(access_info, access.AccessInfoV2) + self.assertEqual(token_id, access_info.auth_token) + + def test_validate_token_access_info_with_access_info(self): + # Can validate a token passing an access info. + token_id = uuid.uuid4().hex + token_fixture = fixture.V2Token(token_id=token_id) + self.stub_url('GET', ['tokens', token_id], json=token_fixture) + token = access.AccessInfo.factory(body=token_fixture) + access_info = self.client.tokens.validate_access_info(token) + self.assertIsInstance(access_info, access.AccessInfoV2) + self.assertEqual(token_id, access_info.auth_token) + + def test_get_revoked(self): + sample_revoked_response = {'signed': '-----BEGIN CMS-----\nMIIB...'} + self.stub_url('GET', ['tokens', 'revoked'], + json=sample_revoked_response) + resp = self.client.tokens.get_revoked() + self.assertEqual(sample_revoked_response, resp) diff --git a/keystoneclient/tests/unit/v2_0/test_users.py b/keystoneclient/tests/unit/v2_0/test_users.py new file mode 100644 index 0000000..455ca3c --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_users.py @@ -0,0 +1,305 @@ +# 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.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import roles +from keystoneclient.v2_0 import users + + +class UserTests(utils.TestCase): + def setUp(self): + super(UserTests, self).setUp() + self.ADMIN_USER_ID = uuid.uuid4().hex + self.DEMO_USER_ID = uuid.uuid4().hex + self.TEST_USERS = { + "users": { + "values": [ + { + "email": "None", + "enabled": True, + "id": self.ADMIN_USER_ID, + "name": "admin", + }, + { + "email": "None", + "enabled": True, + "id": self.DEMO_USER_ID, + "name": "demo", + }, + ] + } + } + + def test_create(self): + tenant_id = uuid.uuid4().hex + user_id = uuid.uuid4().hex + password = uuid.uuid4().hex + req_body = { + "user": { + "name": "gabriel", + "password": password, + "tenantId": tenant_id, + "email": "test@example.com", + "enabled": True, + } + } + + resp_body = { + "user": { + "name": "gabriel", + "enabled": True, + "tenantId": tenant_id, + "id": user_id, + "password": password, + "email": "test@example.com", + } + } + + self.stub_url('POST', ['users'], json=resp_body) + + user = self.client.users.create(req_body['user']['name'], + req_body['user']['password'], + req_body['user']['email'], + tenant_id=req_body['user']['tenantId'], + enabled=req_body['user']['enabled']) + self.assertIsInstance(user, users.User) + self.assertEqual(user.id, user_id) + self.assertEqual(user.name, "gabriel") + self.assertEqual(user.email, "test@example.com") + self.assertRequestBodyIs(json=req_body) + self.assertNotIn(password, self.logger.output) + + def test_create_user_without_email(self): + tenant_id = uuid.uuid4().hex + req_body = { + "user": { + "name": "gabriel", + "password": "test", + "tenantId": tenant_id, + "enabled": True, + "email": None, + } + } + + user_id = uuid.uuid4().hex + resp_body = { + "user": { + "name": "gabriel", + "enabled": True, + "tenantId": tenant_id, + "id": user_id, + "password": "test", + } + } + + self.stub_url('POST', ['users'], json=resp_body) + + user = self.client.users.create( + req_body['user']['name'], + req_body['user']['password'], + tenant_id=req_body['user']['tenantId'], + enabled=req_body['user']['enabled']) + self.assertIsInstance(user, users.User) + self.assertEqual(user.id, user_id) + self.assertEqual(user.name, "gabriel") + self.assertRequestBodyIs(json=req_body) + + def test_create_user_without_password(self): + user_name = 'test' + user_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + user_enabled = True + req_body = { + 'user': { + 'name': user_name, + 'password': None, + 'tenantId': tenant_id, + 'enabled': user_enabled, + 'email': None, + } + } + resp_body = { + 'user': { + 'name': user_name, + 'enabled': user_enabled, + 'tenantId': tenant_id, + 'id': user_id, + } + } + + self.stub_url('POST', ['users'], json=resp_body) + + user = self.client.users.create(user_name, tenant_id=tenant_id, + enabled=user_enabled) + self.assertIsInstance(user, users.User) + self.assertEqual(user_id, user.id) + self.assertEqual(user_name, user.name) + self.assertRequestBodyIs(json=req_body) + + def test_delete(self): + self.stub_url('DELETE', ['users', self.ADMIN_USER_ID], status_code=204) + self.client.users.delete(self.ADMIN_USER_ID) + + def test_get(self): + self.stub_url('GET', ['users', self.ADMIN_USER_ID], + json={'user': self.TEST_USERS['users']['values'][0]}) + + u = self.client.users.get(self.ADMIN_USER_ID) + self.assertIsInstance(u, users.User) + self.assertEqual(u.id, self.ADMIN_USER_ID) + self.assertEqual(u.name, 'admin') + + def test_list(self): + self.stub_url('GET', ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list() + [self.assertIsInstance(u, users.User) for u in user_list] + + def test_list_limit(self): + self.stub_url('GET', ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(limit=1) + self.assertQueryStringIs('limit=1') + [self.assertIsInstance(u, users.User) for u in user_list] + + def test_list_marker(self): + self.stub_url('GET', ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(marker='foo') + self.assertQueryStringIs('marker=foo') + [self.assertIsInstance(u, users.User) for u in user_list] + + def test_list_limit_marker(self): + self.stub_url('GET', ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(limit=1, marker='foo') + + self.assertQueryStringIs('marker=foo&limit=1') + [self.assertIsInstance(u, users.User) for u in user_list] + + def test_update(self): + req_1 = { + "user": { + "id": self.DEMO_USER_ID, + "email": "gabriel@example.com", + "name": "gabriel", + } + } + password = uuid.uuid4().hex + req_2 = { + "user": { + "id": self.DEMO_USER_ID, + "password": password, + } + } + tenant_id = uuid.uuid4().hex + req_3 = { + "user": { + "id": self.DEMO_USER_ID, + "tenantId": tenant_id, + } + } + req_4 = { + "user": { + "id": self.DEMO_USER_ID, + "enabled": False, + } + } + + self.stub_url('PUT', ['users', self.DEMO_USER_ID], json=req_1) + self.stub_url('PUT', + ['users', self.DEMO_USER_ID, 'OS-KSADM', 'password'], + json=req_2) + self.stub_url('PUT', + ['users', self.DEMO_USER_ID, 'OS-KSADM', 'tenant'], + json=req_3) + self.stub_url('PUT', + ['users', self.DEMO_USER_ID, 'OS-KSADM', 'enabled'], + json=req_4) + + self.client.users.update(self.DEMO_USER_ID, + name='gabriel', + email='gabriel@example.com') + self.assertRequestBodyIs(json=req_1) + self.client.users.update_password(self.DEMO_USER_ID, password) + self.assertRequestBodyIs(json=req_2) + self.client.users.update_tenant(self.DEMO_USER_ID, tenant_id) + self.assertRequestBodyIs(json=req_3) + self.client.users.update_enabled(self.DEMO_USER_ID, False) + self.assertRequestBodyIs(json=req_4) + self.assertNotIn(password, self.logger.output) + + def test_update_own_password(self): + old_password = uuid.uuid4().hex + new_password = uuid.uuid4().hex + req_body = { + 'user': { + 'password': new_password, + 'original_password': old_password + } + } + resp_body = { + 'access': {} + } + user_id = uuid.uuid4().hex + self.stub_url('PATCH', ['OS-KSCRUD', 'users', user_id], json=resp_body) + + self.client.user_id = user_id + self.client.users.update_own_password(old_password, new_password) + self.assertRequestBodyIs(json=req_body) + self.assertNotIn(old_password, self.logger.output) + self.assertNotIn(new_password, self.logger.output) + + def test_user_role_listing(self): + user_id = uuid.uuid4().hex + role_id1 = uuid.uuid4().hex + role_id2 = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + user_resp = { + 'user': { + 'id': user_id, + 'email': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + } + } + + roles_resp = { + 'roles': { + 'values': [ + { + 'name': uuid.uuid4().hex, + 'id': role_id1, + }, + { + 'name': uuid.uuid4().hex, + 'id': role_id2, + } + ] + } + } + + self.stub_url('GET', ['users', user_id], json=user_resp) + self.stub_url('GET', + ['tenants', tenant_id, 'users', user_id, 'roles'], + json=roles_resp) + + user = self.client.users.get(user_id) + role_objs = user.list_roles(tenant_id) + + for r in role_objs: + self.assertIsInstance(r, roles.Role) + + self.assertEqual(set([role_id1, role_id2]), + set([r.id for r in role_objs])) diff --git a/keystoneclient/tests/unit/v2_0/utils.py b/keystoneclient/tests/unit/v2_0/utils.py new file mode 100644 index 0000000..475181f --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/utils.py @@ -0,0 +1,88 @@ +# 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.tests.unit import utils +from keystoneclient.v2_0 import client + +TestResponse = utils.TestResponse + + +class UnauthenticatedTestCase(utils.TestCase): + """Class used as base for unauthenticated calls.""" + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') + + +class TestCase(UnauthenticatedTestCase): + + TEST_ADMIN_IDENTITY_ENDPOINT = "http://127.0.0.1:35357/v2.0" + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "adminURL": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8774/v1.0", + "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" + }], + "type": "nova_compat", + "name": "nova_compat" + }, { + "endpoints": [{ + "adminURL": "http://nova/novapi/admin", + "region": "RegionOne", + "internalURL": "http://nova/novapi/internal", + "publicURL": "http://nova/novapi/public" + }], + "type": "compute", + "name": "nova" + }, { + "endpoints": [{ + "adminURL": "http://glance/glanceapi/admin", + "region": "RegionOne", + "internalURL": "http://glance/glanceapi/internal", + "publicURL": "http://glance/glanceapi/public" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "adminURL": TEST_ADMIN_IDENTITY_ENDPOINT, + "region": "RegionOne", + "internalURL": "http://127.0.0.1:5000/v2.0", + "publicURL": "http://127.0.0.1:5000/v2.0" + }], + "type": "identity", + "name": "keystone" + }, { + "endpoints": [{ + "adminURL": "http://swift/swiftapi/admin", + "region": "RegionOne", + "internalURL": "http://swift/swiftapi/internal", + "publicURL": "http://swift/swiftapi/public" + }], + "type": "object-store", + "name": "swift" + }] + + def setUp(self): + super(TestCase, self).setUp() + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url=self.TEST_URL, + endpoint=self.TEST_URL) + + def stub_auth(self, **kwargs): + self.stub_url('POST', ['tokens'], **kwargs) diff --git a/keystoneclient/tests/unit/v3/__init__.py b/keystoneclient/tests/unit/v3/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/keystoneclient/tests/unit/v3/__init__.py diff --git a/keystoneclient/tests/unit/v3/client_fixtures.py b/keystoneclient/tests/unit/v3/client_fixtures.py new file mode 100644 index 0000000..517f9ae --- /dev/null +++ b/keystoneclient/tests/unit/v3/client_fixtures.py @@ -0,0 +1,182 @@ +# 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 __future__ import unicode_literals + +from keystoneclient import fixture + + +def unscoped_token(): + return fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a', + user_name='exampleuser', + user_domain_id='4e6893b7ba0b4006840c3845660b86ed', + user_domain_name='exampledomain', + expires='2010-11-01T03:32:15-05:00') + + +def domain_scoped_token(): + f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a', + user_name='exampleuser', + user_domain_id='4e6893b7ba0b4006840c3845660b86ed', + user_domain_name='exampledomain', + expires='2010-11-01T03:32:15-05:00', + domain_id='8e9283b7ba0b1038840c3842058b86ab', + domain_name='anotherdomain') + + f.add_role(id='76e72a', name='admin') + f.add_role(id='f4f392', name='member') + region = 'RegionOne' + + s = f.add_service('volume') + s.add_standard_endpoints(public='http://public.com:8776/v1/None', + internal='http://internal.com:8776/v1/None', + admin='http://admin.com:8776/v1/None', + region=region) + + s = f.add_service('image') + s.add_standard_endpoints(public='http://public.com:9292/v1', + internal='http://internal:9292/v1', + admin='http://admin:9292/v1', + region=region) + + s = f.add_service('compute') + s.add_standard_endpoints(public='http://public.com:8774/v1.1/None', + internal='http://internal:8774/v1.1/None', + admin='http://admin:8774/v1.1/None', + region=region) + + s = f.add_service('ec2') + s.add_standard_endpoints(public='http://public.com:8773/services/Cloud', + internal='http://internal:8773/services/Cloud', + admin='http://admin:8773/services/Admin', + region=region) + + s = f.add_service('identity') + s.add_standard_endpoints(public='http://public.com:5000/v3', + internal='http://internal:5000/v3', + admin='http://admin:35357/v3', + region=region) + + return f + + +def project_scoped_token(): + f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a', + user_name='exampleuser', + user_domain_id='4e6893b7ba0b4006840c3845660b86ed', + user_domain_name='exampledomain', + expires='2010-11-01T03:32:15-05:00', + project_id='225da22d3ce34b15877ea70b2a575f58', + project_name='exampleproject', + project_domain_id='4e6893b7ba0b4006840c3845660b86ed', + project_domain_name='exampledomain') + + f.add_role(id='76e72a', name='admin') + f.add_role(id='f4f392', name='member') + + region = 'RegionOne' + tenant = '225da22d3ce34b15877ea70b2a575f58' + + s = f.add_service('volume') + s.add_standard_endpoints(public='http://public.com:8776/v1/%s' % tenant, + internal='http://internal:8776/v1/%s' % tenant, + admin='http://admin:8776/v1/%s' % tenant, + region=region) + + s = f.add_service('image') + s.add_standard_endpoints(public='http://public.com:9292/v1', + internal='http://internal:9292/v1', + admin='http://admin:9292/v1', + region=region) + + s = f.add_service('compute') + s.add_standard_endpoints(public='http://public.com:8774/v2/%s' % tenant, + internal='http://internal:8774/v2/%s' % tenant, + admin='http://admin:8774/v2/%s' % tenant, + region=region) + + s = f.add_service('ec2') + s.add_standard_endpoints(public='http://public.com:8773/services/Cloud', + internal='http://internal:8773/services/Cloud', + admin='http://admin:8773/services/Admin', + region=region) + + s = f.add_service('identity') + s.add_standard_endpoints(public='http://public.com:5000/v3', + internal='http://internal:5000/v3', + admin='http://admin:35357/v3', + region=region) + + return f + + +AUTH_SUBJECT_TOKEN = '3e2813b7ba0b4006840c3825860b86ed' + +AUTH_RESPONSE_HEADERS = { + 'X-Subject-Token': AUTH_SUBJECT_TOKEN, +} + + +def auth_response_body(): + f = fixture.V3Token(user_id='567', + user_name='test', + user_domain_id='1', + user_domain_name='aDomain', + expires='2010-11-01T03:32:15-05:00', + project_domain_id='123', + project_domain_name='aDomain', + project_id='345', + project_name='aTenant') + + f.add_role(id='76e72a', name='admin') + f.add_role(id='f4f392', name='member') + + s = f.add_service('compute', name='nova') + s.add_standard_endpoints( + public='https://compute.north.host/novapi/public', + internal='https://compute.north.host/novapi/internal', + admin='https://compute.north.host/novapi/admin', + region='North') + + s = f.add_service('object-store', name='swift') + s.add_standard_endpoints( + public='http://swift.north.host/swiftapi/public', + internal='http://swift.north.host/swiftapi/internal', + admin='http://swift.north.host/swiftapi/admin', + region='South') + + s = f.add_service('image', name='glance') + s.add_standard_endpoints( + public='http://glance.north.host/glanceapi/public', + internal='http://glance.north.host/glanceapi/internal', + admin='http://glance.north.host/glanceapi/admin', + region='North') + + s.add_standard_endpoints( + public='http://glance.south.host/glanceapi/public', + internal='http://glance.south.host/glanceapi/internal', + admin='http://glance.south.host/glanceapi/admin', + region='South') + + return f + + +def trust_token(): + return fixture.V3Token(user_id='0ca8f6', + user_name='exampleuser', + user_domain_id='4e6893b7ba0b4006840c3845660b86ed', + user_domain_name='exampledomain', + expires='2010-11-01T03:32:15-05:00', + trust_id='fe0aef', + trust_impersonation=False, + trustee_user_id='0ca8f6', + trustor_user_id='bd263c') diff --git a/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml b/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml new file mode 100644 index 0000000..487bcac --- /dev/null +++ b/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml @@ -0,0 +1,132 @@ +<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> + <s:Header> + <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal</a:Action> + <a:RelatesTo>urn:uuid:487c064b-b7c6-4654-b4d4-715f9961170e</a:RelatesTo> + <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> + <u:Timestamp u:Id="_0"> + <u:Created>2014-08-05T18:36:14.235Z</u:Created> + <u:Expires>2014-08-05T18:41:14.235Z</u:Expires> + </u:Timestamp> + </o:Security> + </s:Header> + <s:Body> + <trust:RequestSecurityTokenResponseCollection xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512"> + <trust:RequestSecurityTokenResponse> + <trust:Lifetime> + <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-08-05T18:36:14.063Z</wsu:Created> + <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-08-05T19:36:14.063Z</wsu:Expires> + </trust:Lifetime> + <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"> + <wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing"> + <wsa:Address>https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS</wsa:Address> + </wsa:EndpointReference> + </wsp:AppliesTo> + <trust:RequestedSecurityToken> + <saml:Assertion MajorVersion="1" MinorVersion="1" AssertionID="_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f" Issuer="https://cern.ch/login" IssueInstant="2014-08-05T18:36:14.235Z" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"> + <saml:Conditions NotBefore="2014-08-05T18:36:14.063Z" NotOnOrAfter="2014-08-05T19:36:14.063Z"> + <saml:AudienceRestrictionCondition> + <saml:Audience>https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS</saml:Audience> + </saml:AudienceRestrictionCondition> + </saml:Conditions> + <saml:AttributeStatement> + <saml:Subject> + <saml:NameIdentifier Format="http://schemas.xmlsoap.org/claims/UPN">marek.denis@cern.ch</saml:NameIdentifier> + <saml:SubjectConfirmation> + <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod> + </saml:SubjectConfirmation> + </saml:Subject> + <saml:Attribute AttributeName="UPN" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>marek.denis@cern.ch</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="EmailAddress" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>marek.denis@cern.ch</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="CommonName" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>madenis</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="role" AttributeNamespace="http://schemas.microsoft.com/ws/2008/06/identity/claims"> + <saml:AttributeValue>CERN Users</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="Group" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>Domain Users</saml:AttributeValue> + <saml:AttributeValue>occupants-bldg-31</saml:AttributeValue> + <saml:AttributeValue>CERN-Direct-Employees</saml:AttributeValue> + <saml:AttributeValue>ca-dev-allowed</saml:AttributeValue> + <saml:AttributeValue>cernts-cerntstest-users</saml:AttributeValue> + <saml:AttributeValue>staf-fell-pjas-at-cern</saml:AttributeValue> + <saml:AttributeValue>ELG-CERN</saml:AttributeValue> + <saml:AttributeValue>student-club-new-members</saml:AttributeValue> + <saml:AttributeValue>pawel-dynamic-test-82</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="DisplayName" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>Marek Kamil Denis</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="MobileNumber" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>+5555555</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="Building" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>31S-013</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="Firstname" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>Marek Kamil</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="Lastname" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>Denis</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="IdentityClass" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>CERN Registered</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="Federation" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>CERN</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute AttributeName="AuthLevel" AttributeNamespace="http://schemas.xmlsoap.org/claims"> + <saml:AttributeValue>Normal</saml:AttributeValue> + </saml:Attribute> + </saml:AttributeStatement> + <saml:AuthenticationStatement AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password" AuthenticationInstant="2014-08-05T18:36:14.032Z"> + <saml:Subject> + <saml:NameIdentifier Format="http://schemas.xmlsoap.org/claims/UPN">marek.denis@cern.ch</saml:NameIdentifier> + <saml:SubjectConfirmation> + <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod> + </saml:SubjectConfirmation> + </saml:Subject> + </saml:AuthenticationStatement> + <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> + <SignedInfo> + <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /> + <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" /> + <Reference URI="#_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f"> + <Transforms> + <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /> + <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /> + </Transforms> + <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" /> + <DigestValue>EaZ/2d0KAY5un9akV3++Npyk6hBc8JuTYs2S3lSxUeQ=</DigestValue> + </Reference> + </SignedInfo> + <SignatureValue>CxYiYvNsbedhHdmDbb9YQCBy6Ppus3bNJdw2g2HLq0VU2yRhv23mUW05I89Hs4yG4OcCo0uOZ3zaeNFbSNXMW+Mr996tAXtujKjgyrCXNJAToE+gwltvGxwY1EluSbe3IzoSM3Ao87mKhxGOSzlDhuN7dQ9Rv6l/J4gUjbOO5SIX4pdZ6mVF7cHEfe9x+H8Lg15YjnElQUEaPi+NSW5jYTdtIpsB4ORxJvALuSt6+4doDYc9wuwBiWkEdnBHAQBINoKpAV2oy0/C85SBX3IdRhxUznmL5yEUmf8JvPccXecMPqJow0L43mnCdu74xPwU0as3MNfYQ10kLvHXHfIExg==</SignatureValue> + <KeyInfo> + <X509Data> + <X509Certificate>MIIIEjCCBfqgAwIBAgIKLYgjvQAAAAAAMDANBgkqhkiG9w0BAQsFADBRMRIwEAYKCZImiZPyLGQBGRYCY2gxFDASBgoJkiaJk/IsZAEZFgRjZXJuMSUwIwYDVQQDExxDRVJOIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMTEwODA4Mzg1NVoXDTIzMDcyOTA5MTkzOFowVjESMBAGCgmSJomT8ixkARkWAmNoMRQwEgYKCZImiZPyLGQBGRYEY2VybjESMBAGA1UECxMJY29tcHV0ZXJzMRYwFAYDVQQDEw1sb2dpbi5jZXJuLmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp6t1C0SGlLddL2M+ltffGioTnDT3eztOxlA9bAGuvB8/Rjym8en6+ET9boM02CyoR5Vpn8iElXVWccAExPIQEq70D6LPe86vb+tYhuKPeLfuICN9Z0SMQ4f+57vk61Co1/uw/8kPvXlyd+Ai8Dsn/G0hpH67bBI9VOQKfpJqclcSJuSlUB5PJffvMUpr29B0eRx8LKFnIHbDILSu6nVbFLcadtWIjbYvoKorXg3J6urtkz+zEDeYMTvA6ZGOFf/Xy5eGtroSq9csSC976tx+umKEPhXBA9AcpiCV9Cj5axN03Aaa+iTE36jpnjcd9d02dy5Q9jE2nUN6KXnB6qF6eQIDAQABo4ID5TCCA+EwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIg73QCYLtjQ2G7Ysrgd71N4WA0GIehd2yb4Wu9TkCAWQCARkwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIFoDBoBgNVHSAEYTBfMF0GCisGAQQBYAoEAQEwTzBNBggrBgEFBQcCARZBaHR0cDovL2NhLWRvY3MuY2Vybi5jaC9jYS1kb2NzL2NwLWNwcy9jZXJuLXRydXN0ZWQtY2EyLWNwLWNwcy5wZGYwJwYJKwYBBAGCNxUKBBowGDAKBggrBgEFBQcDAjAKBggrBgEFBQcDATAdBgNVHQ4EFgQUqtJcwUXasyM6sRaO5nCMFoFDenMwGAYDVR0RBBEwD4INbG9naW4uY2Vybi5jaDAfBgNVHSMEGDAWgBQdkBnqyM7MPI0UsUzZ7BTiYUADYTCCASoGA1UdHwSCASEwggEdMIIBGaCCARWgggERhkdodHRwOi8vY2FmaWxlcy5jZXJuLmNoL2NhZmlsZXMvY3JsL0NFUk4lMjBDZXJ0aWZpY2F0aW9uJTIwQXV0aG9yaXR5LmNybIaBxWxkYXA6Ly8vQ049Q0VSTiUyMENlcnRpZmljYXRpb24lMjBBdXRob3JpdHksQ049Q0VSTlBLSTA3LENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWNlcm4sREM9Y2g/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIIBVAYIKwYBBQUHAQEEggFGMIIBQjBcBggrBgEFBQcwAoZQaHR0cDovL2NhZmlsZXMuY2Vybi5jaC9jYWZpbGVzL2NlcnRpZmljYXRlcy9DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eS5jcnQwgbsGCCsGAQUFBzAChoGubGRhcDovLy9DTj1DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1jZXJuLERDPWNoP2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jZXJuLmNoL29jc3AwDQYJKoZIhvcNAQELBQADggIBAGKZ3bknTCfNuh4TMaL3PuvBFjU8LQ5NKY9GLZvY2ibYMRk5Is6eWRgyUsy1UJRQdaQQPnnysqrGq8VRw/NIFotBBsA978/+jj7v4e5Kr4o8HvwAQNLBxNmF6XkDytpLL701FcNEGRqIsoIhNzihi2VBADLC9HxljEyPT52IR767TMk/+xTOqClceq3sq6WRD4m+xaWRUJyOhn+Pqr+wbhXIw4wzHC6X0hcLj8P9Povtm6VmKkN9JPuymMo/0+zSrUt2+TYfmbbEKYJSP0+sceQ76IKxxmSdKAr1qDNE8v+c3DvPM2PKmfivwaV2l44FdP8ulzqTgphkYcN1daa9Oc+qJeyu/eL7xWzk6Zq5R+jVrMlM0p1y2XczI7Hoc96TMOcbVnwgMcVqRM9p57VItn6XubYPR0C33i1yUZjkWbIfqEjq6Vev6lVgngOyzu+hqC/8SDyORA3dlF9aZOD13kPZdF/JRphHREQtaRydAiYRlE/WHTvOcY52jujDftUR6oY0eWaWkwSHbX+kDFx8IlR8UtQCUgkGHBGwnOYLIGu7SRDGSfOBOiVhxKoHWVk/pL6eKY2SkmyOmmgO4JnQGg95qeAOMG/EQZt/2x8GAavUqGvYy9dPFwFf08678hQqkjNSuex7UD0ku8OP1QKvpP44l6vZhFc6A5XqjdU9lus1</X509Certificate> + </X509Data> + </KeyInfo> + </Signature> + </saml:Assertion> + </trust:RequestedSecurityToken> + <trust:RequestedAttachedReference> + <o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd"> + <o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f</o:KeyIdentifier> + </o:SecurityTokenReference> + </trust:RequestedAttachedReference> + <trust:RequestedUnattachedReference> + <o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd"> + <o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f</o:KeyIdentifier> + </o:SecurityTokenReference> + </trust:RequestedUnattachedReference> + <trust:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</trust:TokenType> + <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType> + <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType> + </trust:RequestSecurityTokenResponse> + </trust:RequestSecurityTokenResponseCollection> + </s:Body> +</s:Envelope>
\ No newline at end of file diff --git a/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml b/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml new file mode 100644 index 0000000..913252e --- /dev/null +++ b/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml @@ -0,0 +1,19 @@ +<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> + <s:Header> + <a:Action s:mustUnderstand="1">http://www.w3.org/2005/08/addressing/soap/fault</a:Action> + <a:RelatesTo>urn:uuid:89c47849-2622-4cdc-bb06-1d46c89ed12d</a:RelatesTo> + </s:Header> + <s:Body> + <s:Fault> + <s:Code> + <s:Value>s:Sender</s:Value> + <s:Subcode> + <s:Value xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">a:FailedAuthentication</s:Value> + </s:Subcode> + </s:Code> + <s:Reason> + <s:Text xml:lang="en-US">At least one security token in the message could not be validated.</s:Text> + </s:Reason> + </s:Fault> + </s:Body> +</s:Envelope>
\ No newline at end of file diff --git a/keystoneclient/tests/unit/v3/saml2_fixtures.py b/keystoneclient/tests/unit/v3/saml2_fixtures.py new file mode 100644 index 0000000..2ecae6a --- /dev/null +++ b/keystoneclient/tests/unit/v3/saml2_fixtures.py @@ -0,0 +1,171 @@ +# 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 six + +SP_SOAP_RESPONSE = six.b("""<S:Envelope +xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"> +<S:Header> +<paos:Request xmlns:paos="urn:liberty:paos:2003-08" +S:actor="http://schemas.xmlsoap.org/soap/actor/next" +S:mustUnderstand="1" +responseConsumerURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP" +service="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"/> +<ecp:Request xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp" +IsPassive="0" S:actor="http://schemas.xmlsoap.org/soap/actor/next" +S:mustUnderstand="1"> +<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> +https://openstack4.local/shibboleth +</saml:Issuer> +<samlp:IDPList xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> +<samlp:IDPEntry ProviderID="https://idp.testshib.org/idp/shibboleth"/> +</samlp:IDPList></ecp:Request> +<ecp:RelayState xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp" +S:actor="http://schemas.xmlsoap.org/soap/actor/next" S:mustUnderstand="1"> +ss:mem:6f1f20fafbb38433467e9d477df67615</ecp:RelayState> +</S:Header><S:Body><samlp:AuthnRequest +xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" +AssertionConsumerServiceURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP" + ID="_a07186e3992e70e92c17b9d249495643" IssueInstant="2014-06-09T09:48:57Z" + ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Version="2.0"> + <saml:Issuer + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> + https://openstack4.local/shibboleth + </saml:Issuer><samlp:NameIDPolicy AllowCreate="1"/><samlp:Scoping> + <samlp:IDPList> + <samlp:IDPEntry ProviderID="https://idp.testshib.org/idp/shibboleth"/> + </samlp:IDPList></samlp:Scoping></samlp:AuthnRequest></S:Body></S:Envelope> +""") + + +SAML2_ASSERTION = six.b("""<?xml version="1.0" encoding="UTF-8"?> +<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/"> +<soap11:Header> +<ecp:Response xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp" +AssertionConsumerServiceURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP" + soap11:actor="http://schemas.xmlsoap.org/soap/actor/next" + soap11:mustUnderstand="1"/> + <samlec:GeneratedKey xmlns:samlec="urn:ietf:params:xml:ns:samlec" + soap11:actor="http://schemas.xmlsoap.org/soap/actor/next"> + x= + </samlec:GeneratedKey> + </soap11:Header> + <soap11:Body> + <saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" +Destination="https://openstack4.local/Shibboleth.sso/SAML2/ECP" +ID="_bbbe6298d7ee586c915d952013875440" +InResponseTo="_a07186e3992e70e92c17b9d249495643" +IssueInstant="2014-06-09T09:48:58.945Z" Version="2.0"> +<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" +Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"> +https://idp.testshib.org/idp/shibboleth +</saml2:Issuer><saml2p:Status> +<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> +</saml2p:Status> +<saml2:EncryptedAssertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"> +<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" +Id="_e5215ac77a6028a8da8caa8be89bad44" +Type="http://www.w3.org/2001/04/xmlenc#Element"> +<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" +xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"/> +<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> +<xenc:EncryptedKey Id="_204349856f6e73c9480afc949d1b4643" +xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> +<xenc:EncryptionMethod +Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" +xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> +<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" +xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/> +</xenc:EncryptionMethod><ds:KeyInfo><ds:X509Data><ds:X509Certificate> +</ds:X509Certificate> +</ds:X509Data></ds:KeyInfo> +<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> +<xenc:CipherValue>VALUE==</xenc:CipherValue></xenc:CipherData> +</xenc:EncryptedKey></ds:KeyInfo> +<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> +<xenc:CipherValue>VALUE=</xenc:CipherValue></xenc:CipherData> +</xenc:EncryptedData></saml2:EncryptedAssertion></saml2p:Response> +</soap11:Body></soap11:Envelope> +""") + +UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN' + +UNSCOPED_TOKEN = { + "token": { + "issued_at": "2014-06-09T09:48:59.643406Z", + "extras": {}, + "methods": ["saml2"], + "expires_at": "2014-06-09T10:48:59.643375Z", + "user": { + "OS-FEDERATION": { + "identity_provider": { + "id": "testshib" + }, + "protocol": { + "id": "saml2" + }, + "groups": [ + {"id": "1764fa5cf69a49a4918131de5ce4af9a"} + ] + }, + "id": "testhib%20user", + "name": "testhib user" + } + } +} + +PROJECTS = { + "projects": [ + { + "domain_id": "37ef61", + "enabled": 'true', + "id": "12d706", + "links": { + "self": "http://identity:35357/v3/projects/12d706" + }, + "name": "a project name" + }, + { + "domain_id": "37ef61", + "enabled": 'true', + "id": "9ca0eb", + "links": { + "self": "http://identity:35357/v3/projects/9ca0eb" + }, + "name": "another project" + } + ], + "links": { + "self": "http://identity:35357/v3/OS-FEDERATION/projects", + "previous": 'null', + "next": 'null' + } +} + +DOMAINS = { + "domains": [ + { + "description": "desc of domain", + "enabled": 'true', + "id": "37ef61", + "links": { + "self": "http://identity:35357/v3/domains/37ef61" + }, + "name": "my domain" + } + ], + "links": { + "self": "http://identity:35357/v3/OS-FEDERATION/domains", + "previous": 'null', + "next": 'null' + } +} diff --git a/keystoneclient/tests/unit/v3/test_access.py b/keystoneclient/tests/unit/v3/test_access.py new file mode 100644 index 0000000..d3107af --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_access.py @@ -0,0 +1,191 @@ +# 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 datetime +import uuid + +from oslo_utils import timeutils + +from keystoneclient import access +from keystoneclient import fixture +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils + + +TOKEN_RESPONSE = utils.TestResponse({ + "headers": client_fixtures.AUTH_RESPONSE_HEADERS +}) +UNSCOPED_TOKEN = client_fixtures.unscoped_token() +DOMAIN_SCOPED_TOKEN = client_fixtures.domain_scoped_token() +PROJECT_SCOPED_TOKEN = client_fixtures.project_scoped_token() + + +class AccessInfoTest(utils.TestCase): + def test_building_unscoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=UNSCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertNotIn('catalog', auth_ref) + + self.assertEqual(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEqual(auth_ref.username, 'exampleuser') + self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEqual(auth_ref.role_ids, []) + self.assertEqual(auth_ref.role_names, []) + + self.assertIsNone(auth_ref.project_name) + self.assertIsNone(auth_ref.project_id) + + self.assertIsNone(auth_ref.auth_url) + self.assertIsNone(auth_ref.management_url) + + self.assertFalse(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + + self.assertEqual(auth_ref.user_domain_id, + '4e6893b7ba0b4006840c3845660b86ed') + self.assertEqual(auth_ref.user_domain_name, 'exampledomain') + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + + self.assertEqual(auth_ref.expires, timeutils.parse_isotime( + UNSCOPED_TOKEN['token']['expires_at'])) + self.assertEqual(auth_ref.issued, timeutils.parse_isotime( + UNSCOPED_TOKEN['token']['issued_at'])) + + self.assertEqual(auth_ref.expires, UNSCOPED_TOKEN.expires) + self.assertEqual(auth_ref.issued, UNSCOPED_TOKEN.issued) + + def test_will_expire_soon(self): + expires = timeutils.utcnow() + datetime.timedelta(minutes=5) + UNSCOPED_TOKEN['token']['expires_at'] = expires.isoformat() + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=UNSCOPED_TOKEN) + self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) + self.assertTrue(auth_ref.will_expire_soon(stale_duration=300)) + self.assertFalse(auth_ref.will_expire_soon()) + + def test_building_domain_scoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=DOMAIN_SCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertIn('catalog', auth_ref) + self.assertTrue(auth_ref['catalog']) + + self.assertEqual(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEqual(auth_ref.username, 'exampleuser') + self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEqual(auth_ref.role_ids, ['76e72a', 'f4f392']) + self.assertEqual(auth_ref.role_names, ['admin', 'member']) + + self.assertEqual(auth_ref.domain_name, 'anotherdomain') + self.assertEqual(auth_ref.domain_id, + '8e9283b7ba0b1038840c3842058b86ab') + + self.assertIsNone(auth_ref.project_name) + self.assertIsNone(auth_ref.project_id) + + self.assertEqual(auth_ref.user_domain_id, + '4e6893b7ba0b4006840c3845660b86ed') + self.assertEqual(auth_ref.user_domain_name, 'exampledomain') + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + + self.assertTrue(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + + def test_building_project_scoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=PROJECT_SCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertIn('catalog', auth_ref) + self.assertTrue(auth_ref['catalog']) + + self.assertEqual(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEqual(auth_ref.username, 'exampleuser') + self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEqual(auth_ref.role_ids, ['76e72a', 'f4f392']) + self.assertEqual(auth_ref.role_names, ['admin', 'member']) + + self.assertIsNone(auth_ref.domain_name) + self.assertIsNone(auth_ref.domain_id) + + self.assertEqual(auth_ref.project_name, 'exampleproject') + self.assertEqual(auth_ref.project_id, + '225da22d3ce34b15877ea70b2a575f58') + + self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) + self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) + + self.assertEqual(auth_ref.auth_url, + ('http://public.com:5000/v3',)) + self.assertEqual(auth_ref.management_url, + ('http://admin:35357/v3',)) + + self.assertEqual(auth_ref.project_domain_id, + '4e6893b7ba0b4006840c3845660b86ed') + self.assertEqual(auth_ref.project_domain_name, 'exampledomain') + + self.assertEqual(auth_ref.user_domain_id, + '4e6893b7ba0b4006840c3845660b86ed') + self.assertEqual(auth_ref.user_domain_name, 'exampledomain') + + self.assertFalse(auth_ref.domain_scoped) + self.assertTrue(auth_ref.project_scoped) + + def test_oauth_access(self): + consumer_id = uuid.uuid4().hex + access_token_id = uuid.uuid4().hex + + token = fixture.V3Token() + token.set_project_scope() + token.set_oauth(access_token_id=access_token_id, + consumer_id=consumer_id) + + auth_ref = access.AccessInfo.factory(body=token) + + self.assertEqual(consumer_id, auth_ref.oauth_consumer_id) + self.assertEqual(access_token_id, auth_ref.oauth_access_token_id) + + self.assertEqual(consumer_id, auth_ref['OS-OAUTH1']['consumer_id']) + self.assertEqual(access_token_id, + auth_ref['OS-OAUTH1']['access_token_id']) + + def test_override_auth_token(self): + token = fixture.V3Token() + token.set_project_scope() + + new_auth_token = uuid.uuid4().hex + auth_ref = access.AccessInfo.factory(body=token, + auth_token=new_auth_token) + self.assertEqual(new_auth_token, auth_ref.auth_token) + + def test_federated_property_standard_token(self): + """Check if is_federated property returns expected value.""" + token = fixture.V3Token() + token.set_project_scope() + auth_ref = access.AccessInfo.factory(body=token) + self.assertFalse(auth_ref.is_federated) diff --git a/keystoneclient/tests/unit/v3/test_auth.py b/keystoneclient/tests/unit/v3/test_auth.py new file mode 100644 index 0000000..506b026 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_auth.py @@ -0,0 +1,352 @@ +# 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 oslo_serialization import jsonutils + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import client + + +class AuthenticateAgainstKeystoneTests(utils.TestCase): + def setUp(self): + super(AuthenticateAgainstKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "token": { + "methods": [ + "token", + "password" + ], + + "expires_at": "2020-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_TENANT_ID, + "name": self.TEST_TENANT_NAME + }, + "user": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_USER, + "name": self.TEST_USER + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + "catalog": self.TEST_SERVICE_CATALOG + }, + } + self.TEST_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "name": self.TEST_DOMAIN_NAME + }, + "name": self.TEST_USER, + "password": self.TEST_TOKEN + } + } + }, + "scope": { + "project": { + "id": self.TEST_TENANT_ID + }, + } + } + } + self.TEST_REQUEST_HEADERS = { + 'Content-Type': 'application/json', + 'User-Agent': 'python-keystoneclient' + } + self.TEST_RESPONSE_HEADERS = { + 'X-Subject-Token': self.TEST_TOKEN + } + + def test_authenticate_success(self): + TEST_TOKEN = "abcdef" + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + self.stub_auth(json=self.TEST_RESPONSE_DICT, subject_token=TEST_TOKEN) + + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, TEST_TOKEN) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_failure(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + ident['password']['user']['password'] = 'bad_key' + error = {"unauthorized": {"message": "Unauthorized", + "code": "401"}} + + self.stub_auth(status_code=401, json=error) + + # Workaround for issue with assertRaises on python2.6 + # where with assertRaises(exceptions.Unauthorized): doesn't work + # right + def client_create_wrapper(): + client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password="bad_key", + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertRaises(exceptions.Unauthorized, client_create_wrapper) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_auth_redirect(self): + headers = {'Location': self.TEST_ADMIN_URL + '/auth/tokens'} + self.stub_auth(status_code=305, text='Use proxy', headers=headers) + + self.stub_auth(json=self.TEST_RESPONSE_DICT, + base_url=self.TEST_ADMIN_URL) + + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + + def test_authenticate_success_domain_username_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + + def test_authenticate_success_userid_password_domain_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + scope = self.TEST_REQUEST_BODY['auth']['scope'] + del scope['project'] + scope['domain'] = {} + scope['domain']['id'] = self.TEST_DOMAIN_ID + + token = self.TEST_RESPONSE_DICT['token'] + del token['project'] + token['domain'] = {} + token['domain']['id'] = self.TEST_DOMAIN_ID + token['domain']['name'] = self.TEST_DOMAIN_NAME + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + domain_id=self.TEST_DOMAIN_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_domain_id, + self.TEST_DOMAIN_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_userid_password_project_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_tenant_id, + self.TEST_TENANT_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_password_unscoped(self): + del self.TEST_RESPONSE_DICT['token']['catalog'] + del self.TEST_REQUEST_BODY['auth']['scope'] + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertFalse('catalog' in cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_auth_url_token_authentication(self): + fake_token = 'fake_token' + fake_url = '/fake-url' + fake_resp = {'result': True} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [fake_url], json=fake_resp, + base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) + + cl = client.Client(auth_url=self.TEST_URL, + token=fake_token) + body = jsonutils.loads(self.requests.last_request.body) + self.assertEqual(body['auth']['identity']['token']['id'], fake_token) + + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) + + def test_authenticate_success_token_domain_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + + scope = self.TEST_REQUEST_BODY['auth']['scope'] + del scope['project'] + scope['domain'] = {} + scope['domain']['id'] = self.TEST_DOMAIN_ID + + token = self.TEST_RESPONSE_DICT['token'] + del token['project'] + token['domain'] = {} + token['domain']['id'] = self.TEST_DOMAIN_ID + token['domain']['name'] = self.TEST_DOMAIN_NAME + + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + domain_id=self.TEST_DOMAIN_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_domain_id, + self.TEST_DOMAIN_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_token_project_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_tenant_id, + self.TEST_TENANT_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_token_unscoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + del self.TEST_REQUEST_BODY['auth']['scope'] + del self.TEST_RESPONSE_DICT['token']['catalog'] + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertFalse('catalog' in cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_allow_override_of_auth_token(self): + fake_url = '/fake-url' + fake_token = 'fake_token' + fake_resp = {'result': True} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [fake_url], json=fake_resp, + base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + + self.assertEqual(cl.auth_token, self.TEST_TOKEN) + + # the token returned from the authentication will be used + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) + + # then override that token and the new token shall be used + cl.auth_token = fake_token + + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests.last_request.headers.get('X-Auth-Token') + self.assertEqual(fake_token, token) + + # if we clear that overridden token then we fall back to the original + del cl.auth_token + + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) diff --git a/keystoneclient/tests/unit/v3/test_auth_saml2.py b/keystoneclient/tests/unit/v3/test_auth_saml2.py new file mode 100644 index 0000000..c54cf24 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_auth_saml2.py @@ -0,0 +1,623 @@ +# 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 os +import uuid + +from lxml import etree +from oslo_config import fixture as config +import requests +from six.moves import urllib + +from keystoneclient.auth import conf +from keystoneclient.contrib.auth.v3 import saml2 +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import saml2_fixtures +from keystoneclient.tests.unit.v3 import utils + +ROOTDIR = os.path.dirname(os.path.abspath(__file__)) +XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/') + + +def make_oneline(s): + return etree.tostring(etree.XML(s)).replace(b'\n', b'') + + +def _load_xml(filename): + with open(XMLDIR + filename, 'rb') as f: + return make_oneline(f.read()) + + +class AuthenticateviaSAML2Tests(utils.TestCase): + + GROUP = 'auth' + + class _AuthenticatedResponse(object): + headers = { + 'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER + } + + def json(self): + return saml2_fixtures.UNSCOPED_TOKEN + + class _AuthenticatedResponseInvalidJson(_AuthenticatedResponse): + + def json(self): + raise ValueError() + + class _AuthentiatedResponseMissingTokenID(_AuthenticatedResponse): + headers = {} + + def setUp(self): + super(AuthenticateviaSAML2Tests, self).setUp() + + self.conf_fixture = self.useFixture(config.Config()) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.session = session.Session() + + self.ECP_SP_EMPTY_REQUEST_HEADERS = { + 'Accept': 'text/html; application/vnd.paos+xml', + 'PAOS': ('ver="urn:liberty:paos:2003-08";' + '"urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"') + } + + self.ECP_SP_SAML2_REQUEST_HEADERS = { + 'Content-Type': 'application/vnd.paos+xml' + } + + self.ECP_SAML2_NAMESPACES = { + 'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp', + 'S': 'http://schemas.xmlsoap.org/soap/envelope/', + 'paos': 'urn:liberty:paos:2003-08' + } + self.ECP_RELAY_STATE = '//ecp:RelayState' + self.ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:' + 'Request/' + '@responseConsumerURL') + self.ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/' + '@AssertionConsumerServiceURL') + self.IDENTITY_PROVIDER = 'testidp' + self.IDENTITY_PROVIDER_URL = 'http://local.url' + self.PROTOCOL = 'saml2' + self.FEDERATION_AUTH_URL = '%s/%s' % ( + self.TEST_URL, + 'OS-FEDERATION/identity_providers/testidp/protocols/saml2/auth') + self.SHIB_CONSUMER_URL = ('https://openstack4.local/' + 'Shibboleth.sso/SAML2/ECP') + + self.saml2plugin = saml2.Saml2UnscopedToken( + self.TEST_URL, + self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL, + self.TEST_USER, self.TEST_TOKEN) + + def test_conf_params(self): + section = uuid.uuid4().hex + identity_provider = uuid.uuid4().hex + identity_provider_url = uuid.uuid4().hex + username = uuid.uuid4().hex + password = uuid.uuid4().hex + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(saml2.Saml2UnscopedToken.get_options(), + group=section) + self.conf_fixture.config(auth_plugin='v3unscopedsaml', + identity_provider=identity_provider, + identity_provider_url=identity_provider_url, + username=username, + password=password, + group=section) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertEqual(identity_provider, a.identity_provider) + self.assertEqual(identity_provider_url, a.identity_provider_url) + self.assertEqual(username, a.username) + self.assertEqual(password, a.password) + + def test_initial_sp_call(self): + """Test initial call, expect SOAP message.""" + self.requests.get( + self.FEDERATION_AUTH_URL, + content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)) + a = self.saml2plugin._send_service_provider_request(self.session) + + self.assertFalse(a) + + fixture_soap_response = make_oneline( + saml2_fixtures.SP_SOAP_RESPONSE) + + sp_soap_response = make_oneline( + etree.tostring(self.saml2plugin.saml2_authn_request)) + + error_msg = "Expected %s instead of %s" % (fixture_soap_response, + sp_soap_response) + + self.assertEqual(fixture_soap_response, sp_soap_response, error_msg) + + self.assertEqual( + self.saml2plugin.sp_response_consumer_url, self.SHIB_CONSUMER_URL, + "Expected consumer_url set to %s instead of %s" % ( + self.SHIB_CONSUMER_URL, + str(self.saml2plugin.sp_response_consumer_url))) + + def test_initial_sp_call_when_saml_authenticated(self): + self.requests.get( + self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) + + a = self.saml2plugin._send_service_provider_request(self.session) + self.assertTrue(a) + self.assertEqual( + saml2_fixtures.UNSCOPED_TOKEN['token'], + self.saml2plugin.authenticated_response.json()['token']) + self.assertEqual( + saml2_fixtures.UNSCOPED_TOKEN_HEADER, + self.saml2plugin.authenticated_response.headers['X-Subject-Token']) + + def test_get_unscoped_token_when_authenticated(self): + self.requests.get( + self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER, + 'Content-Type': 'application/json'}) + + token, token_body = self.saml2plugin._get_unscoped_token(self.session) + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_body) + + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, token) + + def test_initial_sp_call_invalid_response(self): + """Send initial SP HTTP request and receive wrong server response.""" + self.requests.get(self.FEDERATION_AUTH_URL, + text='NON XML RESPONSE') + + self.assertRaises( + exceptions.AuthorizationFailure, + self.saml2plugin._send_service_provider_request, + self.session) + + def test_send_authn_req_to_idp(self): + self.requests.post(self.IDENTITY_PROVIDER_URL, + content=saml2_fixtures.SAML2_ASSERTION) + + self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL + self.saml2plugin.saml2_authn_request = etree.XML( + saml2_fixtures.SP_SOAP_RESPONSE) + self.saml2plugin._send_idp_saml2_authn_request(self.session) + + idp_response = make_oneline(etree.tostring( + self.saml2plugin.saml2_idp_authn_response)) + + saml2_assertion_oneline = make_oneline( + saml2_fixtures.SAML2_ASSERTION) + error = "Expected %s instead of %s" % (saml2_fixtures.SAML2_ASSERTION, + idp_response) + self.assertEqual(idp_response, saml2_assertion_oneline, error) + + def test_fail_basicauth_idp_authentication(self): + self.requests.post(self.IDENTITY_PROVIDER_URL, status_code=401) + + self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL + self.saml2plugin.saml2_authn_request = etree.XML( + saml2_fixtures.SP_SOAP_RESPONSE) + self.assertRaises( + exceptions.Unauthorized, + self.saml2plugin._send_idp_saml2_authn_request, + self.session) + + def test_mising_username_password_in_plugin(self): + self.assertRaises(TypeError, + saml2.Saml2UnscopedToken, + self.TEST_URL, self.IDENTITY_PROVIDER, + self.IDENTITY_PROVIDER_URL) + + def test_send_authn_response_to_sp(self): + self.requests.post( + self.SHIB_CONSUMER_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) + + self.saml2plugin.relay_state = etree.XML( + saml2_fixtures.SP_SOAP_RESPONSE).xpath( + self.ECP_RELAY_STATE, namespaces=self.ECP_SAML2_NAMESPACES)[0] + + self.saml2plugin.saml2_idp_authn_response = etree.XML( + saml2_fixtures.SAML2_ASSERTION) + + self.saml2plugin.idp_response_consumer_url = self.SHIB_CONSUMER_URL + self.saml2plugin._send_service_provider_saml2_authn_response( + self.session) + token_json = self.saml2plugin.authenticated_response.json()['token'] + token = self.saml2plugin.authenticated_response.headers[ + 'X-Subject-Token'] + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], + token_json) + + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, + token) + + def test_consumer_url_mismatch_success(self): + self.saml2plugin._check_consumer_urls( + self.session, self.SHIB_CONSUMER_URL, + self.SHIB_CONSUMER_URL) + + def test_consumer_url_mismatch(self): + self.requests.post(self.SHIB_CONSUMER_URL) + invalid_consumer_url = uuid.uuid4().hex + self.assertRaises( + exceptions.ValidationError, + self.saml2plugin._check_consumer_urls, + self.session, self.SHIB_CONSUMER_URL, + invalid_consumer_url) + + def test_custom_302_redirection(self): + self.requests.post( + self.SHIB_CONSUMER_URL, + text='BODY', + headers={'location': self.FEDERATION_AUTH_URL}, + status_code=302) + + self.requests.get( + self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) + + self.session.redirect = False + response = self.session.post( + self.SHIB_CONSUMER_URL, data='CLIENT BODY') + self.assertEqual(302, response.status_code) + self.assertEqual(self.FEDERATION_AUTH_URL, + response.headers['location']) + + response = self.saml2plugin._handle_http_302_ecp_redirect( + self.session, response, 'GET') + + self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url) + self.assertEqual('GET', response.request.method) + + def test_end_to_end_workflow(self): + self.requests.get( + self.FEDERATION_AUTH_URL, + content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)) + + self.requests.post(self.IDENTITY_PROVIDER_URL, + content=saml2_fixtures.SAML2_ASSERTION) + + self.requests.post( + self.SHIB_CONSUMER_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER, + 'Content-Type': 'application/json'}) + + self.session.redirect = False + response = self.saml2plugin.get_auth_ref(self.session) + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, + response.auth_token) + + +class ScopeFederationTokenTests(AuthenticateviaSAML2Tests): + + TEST_TOKEN = client_fixtures.AUTH_SUBJECT_TOKEN + + def setUp(self): + super(ScopeFederationTokenTests, self).setUp() + + self.PROJECT_SCOPED_TOKEN_JSON = client_fixtures.project_scoped_token() + self.PROJECT_SCOPED_TOKEN_JSON['methods'] = ['saml2'] + + # for better readability + self.TEST_TENANT_ID = self.PROJECT_SCOPED_TOKEN_JSON.project_id + self.TEST_TENANT_NAME = self.PROJECT_SCOPED_TOKEN_JSON.project_name + + self.DOMAIN_SCOPED_TOKEN_JSON = client_fixtures.domain_scoped_token() + self.DOMAIN_SCOPED_TOKEN_JSON['methods'] = ['saml2'] + + # for better readability + self.TEST_DOMAIN_ID = self.DOMAIN_SCOPED_TOKEN_JSON.domain_id + self.TEST_DOMAIN_NAME = self.DOMAIN_SCOPED_TOKEN_JSON.domain_name + + self.saml2_scope_plugin = saml2.Saml2ScopedToken( + self.TEST_URL, saml2_fixtures.UNSCOPED_TOKEN_HEADER, + project_id=self.TEST_TENANT_ID) + + def test_scope_saml2_token_to_project(self): + self.stub_auth(json=self.PROJECT_SCOPED_TOKEN_JSON) + + token = self.saml2_scope_plugin.get_auth_ref(self.session) + self.assertTrue(token.project_scoped, "Received token is not scoped") + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token) + self.assertEqual(self.TEST_TENANT_ID, token.project_id) + self.assertEqual(self.TEST_TENANT_NAME, token.project_name) + + def test_scope_saml2_token_to_invalid_project(self): + self.stub_auth(status_code=401) + self.saml2_scope_plugin.project_id = uuid.uuid4().hex + self.saml2_scope_plugin.project_name = None + self.assertRaises(exceptions.Unauthorized, + self.saml2_scope_plugin.get_auth_ref, + self.session) + + def test_scope_saml2_token_to_invalid_domain(self): + self.stub_auth(status_code=401) + self.saml2_scope_plugin.project_id = None + self.saml2_scope_plugin.project_name = None + self.saml2_scope_plugin.domain_id = uuid.uuid4().hex + self.saml2_scope_plugin.domain_name = None + self.assertRaises(exceptions.Unauthorized, + self.saml2_scope_plugin.get_auth_ref, + self.session) + + def test_scope_saml2_token_to_domain(self): + self.stub_auth(json=self.DOMAIN_SCOPED_TOKEN_JSON) + token = self.saml2_scope_plugin.get_auth_ref(self.session) + self.assertTrue(token.domain_scoped, "Received token is not scoped") + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token) + self.assertEqual(self.TEST_DOMAIN_ID, token.domain_id) + self.assertEqual(self.TEST_DOMAIN_NAME, token.domain_name) + + def test_dont_set_project_nor_domain(self): + self.saml2_scope_plugin.project_id = None + self.saml2_scope_plugin.domain_id = None + self.assertRaises(exceptions.ValidationError, + saml2.Saml2ScopedToken, + self.TEST_URL, client_fixtures.AUTH_SUBJECT_TOKEN) + + +class AuthenticateviaADFSTests(utils.TestCase): + + GROUP = 'auth' + + NAMESPACES = { + 's': 'http://www.w3.org/2003/05/soap-envelope', + 'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512', + 'wsa': 'http://www.w3.org/2005/08/addressing', + 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy', + 'a': 'http://www.w3.org/2005/08/addressing', + 'o': ('http://docs.oasis-open.org/wss/2004/01/oasis' + '-200401-wss-wssecurity-secext-1.0.xsd') + } + + USER_XPATH = ('/s:Envelope/s:Header' + '/o:Security' + '/o:UsernameToken' + '/o:Username') + PASSWORD_XPATH = ('/s:Envelope/s:Header' + '/o:Security' + '/o:UsernameToken' + '/o:Password') + ADDRESS_XPATH = ('/s:Envelope/s:Body' + '/trust:RequestSecurityToken' + '/wsp:AppliesTo/wsa:EndpointReference' + '/wsa:Address') + TO_XPATH = ('/s:Envelope/s:Header' + '/a:To') + + @property + def _uuid4(self): + return '4b911420-4982-4009-8afc-5c596cd487f5' + + def setUp(self): + super(AuthenticateviaADFSTests, self).setUp() + + self.conf_fixture = self.useFixture(config.Config()) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + self.session = session.Session(session=requests.Session()) + + self.IDENTITY_PROVIDER = 'adfs' + self.IDENTITY_PROVIDER_URL = ('http://adfs.local/adfs/service/trust/13' + '/usernamemixed') + self.FEDERATION_AUTH_URL = '%s/%s' % ( + self.TEST_URL, + 'OS-FEDERATION/identity_providers/adfs/protocols/saml2/auth') + self.SP_ENDPOINT = 'https://openstack4.local/Shibboleth.sso/ADFS' + + self.adfsplugin = saml2.ADFSUnscopedToken( + self.TEST_URL, self.IDENTITY_PROVIDER, + self.IDENTITY_PROVIDER_URL, self.SP_ENDPOINT, + self.TEST_USER, self.TEST_TOKEN) + + self.ADFS_SECURITY_TOKEN_RESPONSE = _load_xml( + 'ADFS_RequestSecurityTokenResponse.xml') + self.ADFS_FAULT = _load_xml('ADFS_fault.xml') + + def test_conf_params(self): + section = uuid.uuid4().hex + identity_provider = uuid.uuid4().hex + identity_provider_url = uuid.uuid4().hex + sp_endpoint = uuid.uuid4().hex + username = uuid.uuid4().hex + password = uuid.uuid4().hex + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(saml2.ADFSUnscopedToken.get_options(), + group=section) + self.conf_fixture.config(auth_plugin='v3unscopedadfs', + identity_provider=identity_provider, + identity_provider_url=identity_provider_url, + service_provider_endpoint=sp_endpoint, + username=username, + password=password, + group=section) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertEqual(identity_provider, a.identity_provider) + self.assertEqual(identity_provider_url, a.identity_provider_url) + self.assertEqual(sp_endpoint, a.service_provider_endpoint) + self.assertEqual(username, a.username) + self.assertEqual(password, a.password) + + def test_get_adfs_security_token(self): + """Test ADFSUnscopedToken._get_adfs_security_token().""" + + self.requests.post( + self.IDENTITY_PROVIDER_URL, + content=make_oneline(self.ADFS_SECURITY_TOKEN_RESPONSE), + status_code=200) + + self.adfsplugin._prepare_adfs_request() + self.adfsplugin._get_adfs_security_token(self.session) + + adfs_response = etree.tostring(self.adfsplugin.adfs_token) + fixture_response = self.ADFS_SECURITY_TOKEN_RESPONSE + + self.assertEqual(fixture_response, adfs_response) + + def test_adfs_request_user(self): + self.adfsplugin._prepare_adfs_request() + user = self.adfsplugin.prepared_request.xpath( + self.USER_XPATH, namespaces=self.NAMESPACES)[0] + self.assertEqual(self.TEST_USER, user.text) + + def test_adfs_request_password(self): + self.adfsplugin._prepare_adfs_request() + password = self.adfsplugin.prepared_request.xpath( + self.PASSWORD_XPATH, namespaces=self.NAMESPACES)[0] + self.assertEqual(self.TEST_TOKEN, password.text) + + def test_adfs_request_to(self): + self.adfsplugin._prepare_adfs_request() + to = self.adfsplugin.prepared_request.xpath( + self.TO_XPATH, namespaces=self.NAMESPACES)[0] + self.assertEqual(self.IDENTITY_PROVIDER_URL, to.text) + + def test_prepare_adfs_request_address(self): + self.adfsplugin._prepare_adfs_request() + address = self.adfsplugin.prepared_request.xpath( + self.ADDRESS_XPATH, namespaces=self.NAMESPACES)[0] + self.assertEqual(self.SP_ENDPOINT, address.text) + + def test_prepare_sp_request(self): + assertion = etree.XML(self.ADFS_SECURITY_TOKEN_RESPONSE) + assertion = assertion.xpath( + saml2.ADFSUnscopedToken.ADFS_ASSERTION_XPATH, + namespaces=saml2.ADFSUnscopedToken.ADFS_TOKEN_NAMESPACES) + assertion = assertion[0] + assertion = etree.tostring(assertion) + + assertion = assertion.replace( + b'http://docs.oasis-open.org/ws-sx/ws-trust/200512', + b'http://schemas.xmlsoap.org/ws/2005/02/trust') + assertion = urllib.parse.quote(assertion) + assertion = 'wa=wsignin1.0&wresult=' + assertion + + self.adfsplugin.adfs_token = etree.XML( + self.ADFS_SECURITY_TOKEN_RESPONSE) + self.adfsplugin._prepare_sp_request() + + self.assertEqual(assertion, self.adfsplugin.encoded_assertion) + + def test_get_adfs_security_token_authn_fail(self): + """Test proper parsing XML fault after bad authentication. + + An exceptions.AuthorizationFailure should be raised including + error message from the XML message indicating where was the problem. + """ + self.requests.post(self.IDENTITY_PROVIDER_URL, + content=make_oneline(self.ADFS_FAULT), + status_code=500) + + self.adfsplugin._prepare_adfs_request() + self.assertRaises(exceptions.AuthorizationFailure, + self.adfsplugin._get_adfs_security_token, + self.session) + # TODO(marek-denis): Python3 tests complain about missing 'message' + # attributes + # self.assertEqual('a:FailedAuthentication', e.message) + + def test_get_adfs_security_token_bad_response(self): + """Test proper handling HTTP 500 and mangled (non XML) response. + + This should never happen yet, keystoneclient should be prepared + and correctly raise exceptions.InternalServerError once it cannot + parse XML fault message + """ + self.requests.post(self.IDENTITY_PROVIDER_URL, + content=b'NOT XML', + status_code=500) + self.adfsplugin._prepare_adfs_request() + self.assertRaises(exceptions.InternalServerError, + self.adfsplugin._get_adfs_security_token, + self.session) + + # TODO(marek-denis): Need to figure out how to properly send cookies + # from the request_uri() method. + def _send_assertion_to_service_provider(self): + """Test whether SP issues a cookie.""" + cookie = uuid.uuid4().hex + + self.requests.post(self.SP_ENDPOINT, + headers={"set-cookie": cookie}, + status_code=302) + + self.adfsplugin.adfs_token = self._build_adfs_request() + self.adfsplugin._prepare_sp_request() + self.adfsplugin._send_assertion_to_service_provider(self.session) + + self.assertEqual(1, len(self.session.session.cookies)) + + def test_send_assertion_to_service_provider_bad_status(self): + self.requests.post(self.SP_ENDPOINT, status_code=500) + + self.adfsplugin.adfs_token = etree.XML( + self.ADFS_SECURITY_TOKEN_RESPONSE) + self.adfsplugin._prepare_sp_request() + + self.assertRaises( + exceptions.InternalServerError, + self.adfsplugin._send_assertion_to_service_provider, + self.session) + + def test_access_sp_no_cookies_fail(self): + # clean cookie jar + self.session.session.cookies = [] + + self.assertRaises(exceptions.AuthorizationFailure, + self.adfsplugin._access_service_provider, + self.session) + + def test_check_valid_token_when_authenticated(self): + self.requests.get(self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers=client_fixtures.AUTH_RESPONSE_HEADERS) + + self.session.session.cookies = [object()] + self.adfsplugin._access_service_provider(self.session) + response = self.adfsplugin.authenticated_response + + self.assertEqual(client_fixtures.AUTH_RESPONSE_HEADERS, + response.headers) + + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], + response.json()['token']) + + def test_end_to_end_workflow(self): + self.requests.post(self.IDENTITY_PROVIDER_URL, + content=self.ADFS_SECURITY_TOKEN_RESPONSE, + status_code=200) + self.requests.post(self.SP_ENDPOINT, + headers={"set-cookie": 'x'}, + status_code=302) + self.requests.get(self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers=client_fixtures.AUTH_RESPONSE_HEADERS) + + # NOTE(marek-denis): We need to mimic this until self.requests can + # issue cookies properly. + self.session.session.cookies = [object()] + token, token_json = self.adfsplugin._get_unscoped_token(self.session) + self.assertEqual(token, client_fixtures.AUTH_SUBJECT_TOKEN) + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json) diff --git a/keystoneclient/tests/unit/v3/test_client.py b/keystoneclient/tests/unit/v3/test_client.py new file mode 100644 index 0000000..6be09c1 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_client.py @@ -0,0 +1,230 @@ +# 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 copy +import json +import uuid + +import six + +from keystoneclient.auth import token_endpoint +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import client + + +class KeystoneClientTest(utils.TestCase): + + def test_unscoped_init(self): + self.stub_auth(json=client_fixtures.unscoped_token()) + + c = client.Client(user_domain_name='exampledomain', + username='exampleuser', + password='password', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEqual(c.auth_user_id, + 'c4da488862bd435c9e6c0275a0d0e49a') + self.assertFalse(c.has_service_catalog()) + + self.assertEqual('c4da488862bd435c9e6c0275a0d0e49a', + c.get_user_id(session=None)) + self.assertIsNone(c.get_project_id(session=None)) + + def test_domain_scoped_init(self): + self.stub_auth(json=client_fixtures.domain_scoped_token()) + + c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', + password='password', + domain_name='exampledomain', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertTrue(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEqual(c.auth_user_id, + 'c4da488862bd435c9e6c0275a0d0e49a') + self.assertEqual(c.auth_domain_id, + '8e9283b7ba0b1038840c3842058b86ab') + + def test_project_scoped_init(self): + self.stub_auth(json=client_fixtures.project_scoped_token()), + + c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', + password='password', + user_domain_name='exampledomain', + project_name='exampleproject', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertTrue(c.auth_ref.project_scoped) + self.assertEqual(c.auth_user_id, + 'c4da488862bd435c9e6c0275a0d0e49a') + self.assertEqual(c.auth_tenant_id, + '225da22d3ce34b15877ea70b2a575f58') + self.assertEqual('c4da488862bd435c9e6c0275a0d0e49a', + c.get_user_id(session=None)) + self.assertEqual('225da22d3ce34b15877ea70b2a575f58', + c.get_project_id(session=None)) + + def test_auth_ref_load(self): + self.stub_auth(json=client_fixtures.project_scoped_token()) + + c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', + password='password', + project_id='225da22d3ce34b15877ea70b2a575f58', + auth_url=self.TEST_URL) + cache = json.dumps(c.auth_ref) + new_client = client.Client(auth_ref=json.loads(cache)) + self.assertIsNotNone(new_client.auth_ref) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertEqual(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v3') + + def test_auth_ref_load_with_overridden_arguments(self): + new_auth_url = 'https://newkeystone.com/v3' + + self.stub_auth(json=client_fixtures.project_scoped_token()) + self.stub_auth(json=client_fixtures.project_scoped_token(), + base_url=new_auth_url) + + c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', + password='password', + project_id='225da22d3ce34b15877ea70b2a575f58', + auth_url=self.TEST_URL) + cache = json.dumps(c.auth_ref) + new_client = client.Client(auth_ref=json.loads(cache), + auth_url=new_auth_url) + self.assertIsNotNone(new_client.auth_ref) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertEqual(new_client.auth_url, new_auth_url) + self.assertEqual(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v3') + + def test_trust_init(self): + self.stub_auth(json=client_fixtures.trust_token()) + + c = client.Client(user_domain_name='exampledomain', + username='exampleuser', + password='password', + auth_url=self.TEST_URL, + trust_id='fe0aef') + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEqual(c.auth_ref.trust_id, 'fe0aef') + self.assertEqual(c.auth_ref.trustee_user_id, '0ca8f6') + self.assertEqual(c.auth_ref.trustor_user_id, 'bd263c') + self.assertTrue(c.auth_ref.trust_scoped) + self.assertEqual(c.auth_user_id, '0ca8f6') + + def test_init_err_no_auth_url(self): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + username='exampleuser', + password='password') + + def _management_url_is_updated(self, fixture, **kwargs): + second = copy.deepcopy(fixture) + first_url = 'http://admin:35357/v3' + second_url = "http://secondurl:%d/v3'" + + for entry in second['token']['catalog']: + if entry['type'] == 'identity': + entry['endpoints'] = [{ + 'url': second_url % 5000, + 'region': 'RegionOne', + 'interface': 'public' + }, { + 'url': second_url % 5000, + 'region': 'RegionOne', + 'interface': 'internal' + }, { + 'url': second_url % 35357, + 'region': 'RegionOne', + 'interface': 'admin' + }] + + self.stub_auth(response_list=[{'json': fixture}, {'json': second}]) + + cl = client.Client(username='exampleuser', + password='password', + auth_url=self.TEST_URL, + **kwargs) + self.assertEqual(cl.management_url, first_url) + + cl.authenticate() + self.assertEqual(cl.management_url, second_url % 35357) + + def test_management_url_is_updated_with_project(self): + self._management_url_is_updated(client_fixtures.project_scoped_token(), + project_name='exampleproject') + + def test_management_url_is_updated_with_domain(self): + self._management_url_is_updated(client_fixtures.domain_scoped_token(), + domain_name='exampledomain') + + def test_client_with_region_name_passes_to_service_catalog(self): + # NOTE(jamielennox): this is deprecated behaviour that should be + # removed ASAP, however must remain compatible. + + self.stub_auth(json=client_fixtures.auth_response_body()) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL, + region_name='North') + self.assertEqual(cl.service_catalog.url_for(service_type='image'), + 'http://glance.north.host/glanceapi/public') + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL, + region_name='South') + self.assertEqual(cl.service_catalog.url_for(service_type='image'), + 'http://glance.south.host/glanceapi/public') + + def test_client_without_auth_params(self): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + project_name='exampleproject', + auth_url=self.TEST_URL) + + def test_client_params(self): + opts = {'auth': token_endpoint.Token('a', 'b'), + 'connect_retries': 50, + 'endpoint_override': uuid.uuid4().hex, + 'interface': uuid.uuid4().hex, + 'region_name': uuid.uuid4().hex, + 'service_name': uuid.uuid4().hex, + 'user_agent': uuid.uuid4().hex, + } + + sess = session.Session() + cl = client.Client(session=sess, **opts) + + for k, v in six.iteritems(opts): + self.assertEqual(v, getattr(cl._adapter, k)) + + self.assertEqual('identity', cl._adapter.service_type) + self.assertEqual('v3', cl._adapter.version) diff --git a/keystoneclient/tests/unit/v3/test_credentials.py b/keystoneclient/tests/unit/v3/test_credentials.py new file mode 100644 index 0000000..752f25a --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_credentials.py @@ -0,0 +1,54 @@ +# 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.tests.unit.v3 import utils +from keystoneclient.v3 import credentials + + +class CredentialTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(CredentialTests, self).setUp() + self.key = 'credential' + self.collection_key = 'credentials' + self.model = credentials.Credential + self.manager = self.client.credentials + + def new_ref(self, **kwargs): + kwargs = super(CredentialTests, self).new_ref(**kwargs) + kwargs.setdefault('blob', uuid.uuid4().hex) + kwargs.setdefault('project_id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('user_id', uuid.uuid4().hex) + return kwargs + + @staticmethod + def _ref_data_not_blob(ref): + ret_ref = ref.copy() + ret_ref['data'] = ref['blob'] + del ret_ref['blob'] + return ret_ref + + def test_create_data_not_blob(self): + # Test create operation with previous, deprecated "data" argument, + # which should be translated into "blob" at the API call level + req_ref = self.new_ref() + api_ref = self._ref_data_not_blob(req_ref) + req_ref.pop('id') + self.test_create(api_ref, req_ref) + + def test_update_data_not_blob(self): + # Likewise test update operation with data instead of blob argument + req_ref = self.new_ref() + api_ref = self._ref_data_not_blob(req_ref) + self.test_update(api_ref, req_ref) diff --git a/keystoneclient/tests/unit/v3/test_discover.py b/keystoneclient/tests/unit/v3/test_discover.py new file mode 100644 index 0000000..f73c3d7 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_discover.py @@ -0,0 +1,80 @@ +# 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.generic import client +from keystoneclient.tests.unit.v3 import utils + + +class DiscoverKeystoneTests(utils.UnauthenticatedTestCase): + def setUp(self): + super(DiscoverKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "versions": { + "values": [{"id": "v3.0", + "status": "beta", + "updated": "2013-03-06T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:5000/v3.0/", }, + {"rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/3/" + "content/", }, + {"rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/3/" + "identity-dev-guide-3.pdf", }, + ]}, + {"id": "v2.0", + "status": "beta", + "updated": "2013-03-06T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:5000/v2.0/", }, + {"rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/" + "content/", }, + {"rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/" + "identity-dev-guide-2.0.pdf", } + ]}], + }, + } + self.TEST_REQUEST_HEADERS = { + 'User-Agent': 'python-keystoneclient', + 'Accept': 'application/json', + } + + def test_get_version_local(self): + self.requests.get("http://localhost:35357/", + status_code=300, + json=self.TEST_RESPONSE_DICT) + + cs = client.Client() + versions = cs.discover() + self.assertIsInstance(versions, dict) + self.assertIn('message', versions) + self.assertIn('v3.0', versions) + self.assertEqual( + versions['v3.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] + ['href']) + self.assertEqual( + versions['v2.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][1]['links'][0] + ['href']) diff --git a/keystoneclient/tests/unit/v3/test_domains.py b/keystoneclient/tests/unit/v3/test_domains.py new file mode 100644 index 0000000..9cc23e7 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_domains.py @@ -0,0 +1,47 @@ +# 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.tests.unit.v3 import utils +from keystoneclient.v3 import domains + + +class DomainTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(DomainTests, self).setUp() + self.key = 'domain' + self.collection_key = 'domains' + self.model = domains.Domain + self.manager = self.client.domains + + def new_ref(self, **kwargs): + kwargs = super(DomainTests, self).new_ref(**kwargs) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_list_filter_name(self): + super(DomainTests, self).test_list(name='adomain123') + + def test_list_filter_enabled(self): + super(DomainTests, self).test_list(enabled=True) + + def test_list_filter_disabled(self): + # False is converted to '0' ref bug #1267530 + expected_query = {'enabled': '0'} + super(DomainTests, self).test_list(expected_query=expected_query, + enabled=False) + + def test_update_enabled_defaults_to_none(self): + super(DomainTests, self).test_update( + req_ref={'name': uuid.uuid4().hex}) diff --git a/keystoneclient/tests/unit/v3/test_endpoint_filter.py b/keystoneclient/tests/unit/v3/test_endpoint_filter.py new file mode 100644 index 0000000..eaca264 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_endpoint_filter.py @@ -0,0 +1,149 @@ +# Copyright 2014 OpenStack Foundation +# +# 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.tests.unit.v3 import utils + + +class EndpointTestUtils(object): + """Mixin class with shared methods between Endpoint Filter & Policy.""" + + def new_ref(self, **kwargs): + # copied from CrudTests as we need to create endpoint and project + # refs for our tests. EndpointFilter is not exactly CRUD API. + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def new_endpoint_ref(self, **kwargs): + # copied from EndpointTests as we need endpoint refs for our tests + kwargs = self.new_ref(**kwargs) + kwargs.setdefault('interface', 'public') + kwargs.setdefault('region', uuid.uuid4().hex) + kwargs.setdefault('service_id', uuid.uuid4().hex) + kwargs.setdefault('url', uuid.uuid4().hex) + return kwargs + + +class EndpointFilterTests(utils.TestCase, EndpointTestUtils): + """Test project-endpoint associations (a.k.a. EndpointFilter Extension). + + Endpoint filter provides associations between service endpoints and + projects. These assciations are then used to create ad-hoc catalogs for + each project-scoped token request. + + """ + + def setUp(self): + super(EndpointFilterTests, self).setUp() + self.manager = self.client.endpoint_filter + + def new_project_ref(self, **kwargs): + # copied from ProjectTests as we need project refs for our tests + kwargs = self.new_ref(**kwargs) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_add_endpoint_to_project_via_id(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('PUT', + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status_code=201) + + self.manager.add_endpoint_to_project(project=project_id, + endpoint=endpoint_id) + + def test_add_endpoint_to_project_via_obj(self): + project_ref = self.new_project_ref() + endpoint_ref = self.new_endpoint_ref() + project = self.client.projects.resource_class(self.client.projects, + project_ref, + loaded=True) + endpoint = self.client.endpoints.resource_class(self.client.endpoints, + endpoint_ref, + loaded=True) + + self.stub_url('PUT', + [self.manager.OS_EP_FILTER_EXT, + 'projects', project_ref['id'], + 'endpoints', endpoint_ref['id']], + status_code=201) + + self.manager.add_endpoint_to_project(project=project, + endpoint=endpoint) + + def test_delete_endpoint_from_project(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('DELETE', + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status_code=201) + + self.manager.delete_endpoint_from_project(project=project_id, + endpoint=endpoint_id) + + def test_check_endpoint_in_project(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('HEAD', + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status_code=201) + + self.manager.check_endpoint_in_project(project=project_id, + endpoint=endpoint_id) + + def test_list_endpoints_for_project(self): + project_id = uuid.uuid4().hex + endpoints = {'endpoints': [self.new_endpoint_ref(), + self.new_endpoint_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints'], + json=endpoints, + status_code=200) + + endpoints_resp = self.manager.list_endpoints_for_project( + project=project_id) + + expected_endpoint_ids = [ + endpoint['id'] for endpoint in endpoints['endpoints']] + actual_endpoint_ids = [endpoint.id for endpoint in endpoints_resp] + self.assertEqual(expected_endpoint_ids, actual_endpoint_ids) + + def test_list_projects_for_endpoint(self): + endpoint_id = uuid.uuid4().hex + projects = {'projects': [self.new_project_ref(), + self.new_project_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'endpoints', endpoint_id, + 'projects'], + json=projects, + status_code=200) + + projects_resp = self.manager.list_projects_for_endpoint( + endpoint=endpoint_id) + + expected_project_ids = [ + project['id'] for project in projects['projects']] + actual_project_ids = [project.id for project in projects_resp] + self.assertEqual(expected_project_ids, actual_project_ids) diff --git a/keystoneclient/tests/unit/v3/test_endpoint_policy.py b/keystoneclient/tests/unit/v3/test_endpoint_policy.py new file mode 100644 index 0000000..ce9efb4 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_endpoint_policy.py @@ -0,0 +1,242 @@ +# Copyright 2014 IBM Corp. +# +# 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.tests.unit.v3 import test_endpoint_filter +from keystoneclient.tests.unit.v3 import utils + + +class EndpointPolicyTests(utils.TestCase, + test_endpoint_filter.EndpointTestUtils): + """Test policy-endpoint associations (a.k.a. EndpointPolicy Extension).""" + + def setUp(self): + super(EndpointPolicyTests, self).setUp() + self.manager = self.client.endpoint_policy + + def new_policy_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('blob', uuid.uuid4().hex) + return kwargs + + def new_region_ref(self, **kwargs): + kwargs = self.new_ref(**kwargs) + return kwargs + + def new_service_ref(self, **kwargs): + kwargs = self.new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + return kwargs + + def _crud_policy_association_for_endpoint_via_id( + self, http_action, manager_action): + policy_id = uuid.uuid4().hex + endpoint_id = uuid.uuid4().hex + + self.stub_url(http_action, + ['policies', policy_id, self.manager.OS_EP_POLICY_EXT, + 'endpoints', endpoint_id], + status_code=204) + manager_action(policy=policy_id, endpoint=endpoint_id) + + def _crud_policy_association_for_endpoint_via_obj( + self, http_action, manager_action): + policy_ref = self.new_policy_ref() + endpoint_ref = self.new_endpoint_ref() + policy = self.client.policies.resource_class( + self.client.policies, policy_ref, loaded=True) + endpoint = self.client.endpoints.resource_class( + self.client.endpoints, endpoint_ref, loaded=True) + + self.stub_url(http_action, + ['policies', policy_ref['id'], + self.manager.OS_EP_POLICY_EXT, + 'endpoints', endpoint_ref['id']], + status_code=204) + manager_action(policy=policy, endpoint=endpoint) + + def test_create_policy_association_for_endpoint_via_id(self): + self._crud_policy_association_for_endpoint_via_id( + 'PUT', self.manager.create_policy_association_for_endpoint) + + def test_create_policy_association_for_endpoint_via_obj(self): + self._crud_policy_association_for_endpoint_via_obj( + 'PUT', self.manager.create_policy_association_for_endpoint) + + def test_check_policy_association_for_endpoint_via_id(self): + self._crud_policy_association_for_endpoint_via_id( + 'HEAD', self.manager.check_policy_association_for_endpoint) + + def test_check_policy_association_for_endpoint_via_obj(self): + self._crud_policy_association_for_endpoint_via_obj( + 'HEAD', self.manager.check_policy_association_for_endpoint) + + def test_delete_policy_association_for_endpoint_via_id(self): + self._crud_policy_association_for_endpoint_via_id( + 'DELETE', self.manager.delete_policy_association_for_endpoint) + + def test_delete_policy_association_for_endpoint_via_obj(self): + self._crud_policy_association_for_endpoint_via_obj( + 'DELETE', self.manager.delete_policy_association_for_endpoint) + + def _crud_policy_association_for_service_via_id( + self, http_action, manager_action): + policy_id = uuid.uuid4().hex + service_id = uuid.uuid4().hex + + self.stub_url(http_action, + ['policies', policy_id, self.manager.OS_EP_POLICY_EXT, + 'services', service_id], + status_code=204) + manager_action(policy=policy_id, service=service_id) + + def _crud_policy_association_for_service_via_obj( + self, http_action, manager_action): + policy_ref = self.new_policy_ref() + service_ref = self.new_service_ref() + policy = self.client.policies.resource_class( + self.client.policies, policy_ref, loaded=True) + service = self.client.services.resource_class( + self.client.services, service_ref, loaded=True) + + self.stub_url(http_action, + ['policies', policy_ref['id'], + self.manager.OS_EP_POLICY_EXT, + 'services', service_ref['id']], + status_code=204) + manager_action(policy=policy, service=service) + + def test_create_policy_association_for_service_via_id(self): + self._crud_policy_association_for_service_via_id( + 'PUT', self.manager.create_policy_association_for_service) + + def test_create_policy_association_for_service_via_obj(self): + self._crud_policy_association_for_service_via_obj( + 'PUT', self.manager.create_policy_association_for_service) + + def test_check_policy_association_for_service_via_id(self): + self._crud_policy_association_for_service_via_id( + 'HEAD', self.manager.check_policy_association_for_service) + + def test_check_policy_association_for_service_via_obj(self): + self._crud_policy_association_for_service_via_obj( + 'HEAD', self.manager.check_policy_association_for_service) + + def test_delete_policy_association_for_service_via_id(self): + self._crud_policy_association_for_service_via_id( + 'DELETE', self.manager.delete_policy_association_for_service) + + def test_delete_policy_association_for_service_via_obj(self): + self._crud_policy_association_for_service_via_obj( + 'DELETE', self.manager.delete_policy_association_for_service) + + def _crud_policy_association_for_region_and_service_via_id( + self, http_action, manager_action): + policy_id = uuid.uuid4().hex + region_id = uuid.uuid4().hex + service_id = uuid.uuid4().hex + + self.stub_url(http_action, + ['policies', policy_id, self.manager.OS_EP_POLICY_EXT, + 'services', service_id, 'regions', region_id], + status_code=204) + manager_action(policy=policy_id, region=region_id, service=service_id) + + def _crud_policy_association_for_region_and_service_via_obj( + self, http_action, manager_action): + policy_ref = self.new_policy_ref() + region_ref = self.new_region_ref() + service_ref = self.new_service_ref() + policy = self.client.policies.resource_class( + self.client.policies, policy_ref, loaded=True) + region = self.client.regions.resource_class( + self.client.regions, region_ref, loaded=True) + service = self.client.services.resource_class( + self.client.services, service_ref, loaded=True) + + self.stub_url(http_action, + ['policies', policy_ref['id'], + self.manager.OS_EP_POLICY_EXT, + 'services', service_ref['id'], + 'regions', region_ref['id']], + status_code=204) + manager_action(policy=policy, region=region, service=service) + + def test_create_policy_association_for_region_and_service_via_id(self): + self._crud_policy_association_for_region_and_service_via_id( + 'PUT', + self.manager.create_policy_association_for_region_and_service) + + def test_create_policy_association_for_region_and_service_via_obj(self): + self._crud_policy_association_for_region_and_service_via_obj( + 'PUT', + self.manager.create_policy_association_for_region_and_service) + + def test_check_policy_association_for_region_and_service_via_id(self): + self._crud_policy_association_for_region_and_service_via_id( + 'HEAD', + self.manager.check_policy_association_for_region_and_service) + + def test_check_policy_association_for_region_and_service_via_obj(self): + self._crud_policy_association_for_region_and_service_via_obj( + 'HEAD', + self.manager.check_policy_association_for_region_and_service) + + def test_delete_policy_association_for_region_and_service_via_id(self): + self._crud_policy_association_for_region_and_service_via_id( + 'DELETE', + self.manager.delete_policy_association_for_region_and_service) + + def test_delete_policy_association_for_region_and_service_via_obj(self): + self._crud_policy_association_for_region_and_service_via_obj( + 'DELETE', + self.manager.delete_policy_association_for_region_and_service) + + def test_get_policy_for_endpoint(self): + endpoint_id = uuid.uuid4().hex + expected_policy = self.new_policy_ref() + + self.stub_url('GET', + ['endpoints', endpoint_id, self.manager.OS_EP_POLICY_EXT, + 'policy'], + json={'policy': expected_policy}, + status_code=200) + + policy_resp = self.manager.get_policy_for_endpoint( + endpoint=endpoint_id) + + self.assertEqual(expected_policy['id'], policy_resp.id) + self.assertEqual(expected_policy['blob'], policy_resp.blob) + self.assertEqual(expected_policy['type'], policy_resp.type) + + def test_list_endpoints_for_policy(self): + policy_id = uuid.uuid4().hex + endpoints = {'endpoints': [self.new_endpoint_ref(), + self.new_endpoint_ref()]} + self.stub_url('GET', + ['policies', policy_id, self.manager.OS_EP_POLICY_EXT, + 'endpoints'], + json=endpoints, + status_code=200) + + endpoints_resp = self.manager.list_endpoints_for_policy( + policy=policy_id) + + expected_endpoint_ids = [ + endpoint['id'] for endpoint in endpoints['endpoints']] + actual_endpoint_ids = [endpoint.id for endpoint in endpoints_resp] + self.assertEqual(expected_endpoint_ids, actual_endpoint_ids) diff --git a/keystoneclient/tests/unit/v3/test_endpoints.py b/keystoneclient/tests/unit/v3/test_endpoints.py new file mode 100644 index 0000000..000718a --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_endpoints.py @@ -0,0 +1,91 @@ +# 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 import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import endpoints + + +class EndpointTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(EndpointTests, self).setUp() + self.key = 'endpoint' + self.collection_key = 'endpoints' + self.model = endpoints.Endpoint + self.manager = self.client.endpoints + + def new_ref(self, **kwargs): + kwargs = super(EndpointTests, self).new_ref(**kwargs) + kwargs.setdefault('interface', 'public') + kwargs.setdefault('region', uuid.uuid4().hex) + kwargs.setdefault('service_id', uuid.uuid4().hex) + kwargs.setdefault('url', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def test_create_public_interface(self): + ref = self.new_ref(interface='public') + self.test_create(ref) + + def test_create_admin_interface(self): + ref = self.new_ref(interface='admin') + self.test_create(ref) + + def test_create_internal_interface(self): + ref = self.new_ref(interface='internal') + self.test_create(ref) + + def test_create_invalid_interface(self): + ref = self.new_ref(interface=uuid.uuid4().hex) + self.assertRaises(exceptions.ValidationError, self.manager.create, + **utils.parameterize(ref)) + + def test_update_public_interface(self): + ref = self.new_ref(interface='public') + self.test_update(ref) + + def test_update_admin_interface(self): + ref = self.new_ref(interface='admin') + self.test_update(ref) + + def test_update_internal_interface(self): + ref = self.new_ref(interface='internal') + self.test_update(ref) + + def test_update_invalid_interface(self): + ref = self.new_ref(interface=uuid.uuid4().hex) + ref['endpoint'] = "fake_endpoint" + self.assertRaises(exceptions.ValidationError, self.manager.update, + **utils.parameterize(ref)) + + def test_list_public_interface(self): + interface = 'public' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_admin_interface(self): + interface = 'admin' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_internal_interface(self): + interface = 'admin' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_invalid_interface(self): + interface = uuid.uuid4().hex + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.assertRaises(exceptions.ValidationError, self.manager.list, + expected_path=expected_path, interface=interface) diff --git a/keystoneclient/tests/unit/v3/test_federation.py b/keystoneclient/tests/unit/v3/test_federation.py new file mode 100644 index 0000000..19ec44f --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_federation.py @@ -0,0 +1,409 @@ +# 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 copy +import uuid + +from keystoneclient import access +from keystoneclient import exceptions +from keystoneclient import fixture +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib.federation import base +from keystoneclient.v3.contrib.federation import identity_providers +from keystoneclient.v3.contrib.federation import mappings +from keystoneclient.v3.contrib.federation import protocols +from keystoneclient.v3 import domains +from keystoneclient.v3 import projects + + +class IdentityProviderTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(IdentityProviderTests, self).setUp() + self.key = 'identity_provider' + self.collection_key = 'identity_providers' + self.model = identity_providers.IdentityProvider + self.manager = self.client.federation.identity_providers + self.path_prefix = 'OS-FEDERATION' + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def test_positional_parameters_expect_fail(self): + """Ensure CrudManager raises TypeError exceptions. + + After passing wrong number of positional arguments + an exception should be raised. + + Operations to be tested: + * create() + * get() + * list() + * delete() + * update() + + """ + POS_PARAM_1 = uuid.uuid4().hex + POS_PARAM_2 = uuid.uuid4().hex + POS_PARAM_3 = uuid.uuid4().hex + + PARAMETERS = { + 'create': (POS_PARAM_1, POS_PARAM_2), + 'get': (POS_PARAM_1, POS_PARAM_2), + 'list': (POS_PARAM_1, POS_PARAM_2), + 'update': (POS_PARAM_1, POS_PARAM_2, POS_PARAM_3), + 'delete': (POS_PARAM_1, POS_PARAM_2) + } + + for f_name, args in PARAMETERS.items(): + self.assertRaises(TypeError, getattr(self.manager, f_name), + *args) + + def test_create(self, ref=None, req_ref=None): + ref = ref or self.new_ref() + + # req_ref argument allows you to specify a different + # signature for the request when the manager does some + # conversion before doing the request (e.g. converting + # from datetime object to timestamp string) + req_ref = (req_ref or ref).copy() + req_ref.pop('id') + + self.stub_entity('PUT', entity=ref, id=ref['id'], status_code=201) + + returned = self.manager.create(**ref) + self.assertIsInstance(returned, self.model) + for attr in req_ref: + self.assertEqual( + getattr(returned, attr), + req_ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + +class MappingTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(MappingTests, self).setUp() + self.key = 'mapping' + self.collection_key = 'mappings' + self.model = mappings.Mapping + self.manager = self.client.federation.mappings + self.path_prefix = 'OS-FEDERATION' + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('rules', [uuid.uuid4().hex, + uuid.uuid4().hex]) + return kwargs + + def test_create(self, ref=None, req_ref=None): + ref = ref or self.new_ref() + manager_ref = ref.copy() + mapping_id = manager_ref.pop('id') + + # req_ref argument allows you to specify a different + # signature for the request when the manager does some + # conversion before doing the request (e.g. converting + # from datetime object to timestamp string) + req_ref = (req_ref or ref).copy() + + self.stub_entity('PUT', entity=req_ref, id=mapping_id, + status_code=201) + + returned = self.manager.create(mapping_id=mapping_id, **manager_ref) + self.assertIsInstance(returned, self.model) + for attr in req_ref: + self.assertEqual( + getattr(returned, attr), + req_ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(manager_ref) + + +class ProtocolTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ProtocolTests, self).setUp() + self.key = 'protocol' + self.collection_key = 'protocols' + self.model = protocols.Protocol + self.manager = self.client.federation.protocols + self.path_prefix = 'OS-FEDERATION/identity_providers' + + def _transform_to_response(self, ref): + """Rebuild dictionary so it can be used as a + reference response body. + + """ + response = copy.deepcopy(ref) + response['id'] = response.pop('protocol_id') + del response['identity_provider'] + return response + + def new_ref(self, **kwargs): + kwargs.setdefault('mapping', uuid.uuid4().hex) + kwargs.setdefault('identity_provider', uuid.uuid4().hex) + kwargs.setdefault('protocol_id', uuid.uuid4().hex) + return kwargs + + def build_parts(self, identity_provider, protocol_id=None): + """Build array used to construct mocking URL. + + Construct and return array with URL parts later used + by methods like utils.TestCase.stub_entity(). + Example of URL: + ``OS-FEDERATION/identity_providers/{idp_id}/ + protocols/{protocol_id}`` + + """ + parts = ['OS-FEDERATION', 'identity_providers', + identity_provider, 'protocols'] + if protocol_id: + parts.append(protocol_id) + return parts + + def test_build_url_provide_base_url(self): + base_url = uuid.uuid4().hex + parameters = {'base_url': base_url} + url = self.manager.build_url(dict_args_in_out=parameters) + self.assertEqual('/'.join([base_url, self.collection_key]), url) + + def test_build_url_w_idp_id(self): + """Test whether kwargs ``base_url`` discards object's base_url + + This test shows, that when ``base_url`` is specified in the + dict_args_in_out dictionary, values like ``identity_provider_id`` + are not taken into consideration while building the url. + + """ + base_url, identity_provider_id = uuid.uuid4().hex, uuid.uuid4().hex + parameters = { + 'base_url': base_url, + 'identity_provider_id': identity_provider_id + } + url = self.manager.build_url(dict_args_in_out=parameters) + self.assertEqual('/'.join([base_url, self.collection_key]), url) + + def test_build_url_default_base_url(self): + identity_provider_id = uuid.uuid4().hex + parameters = { + 'identity_provider_id': identity_provider_id + } + + url = self.manager.build_url(dict_args_in_out=parameters) + self.assertEqual( + '/'.join([self.manager.base_url, identity_provider_id, + self.manager.collection_key]), url) + + def test_create(self): + """Test creating federation protocol tied to an Identity Provider. + + URL to be tested: PUT /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + """ + request_args = self.new_ref() + expected = self._transform_to_response(request_args) + parts = self.build_parts(request_args['identity_provider'], + request_args['protocol_id']) + self.stub_entity('PUT', entity=expected, + parts=parts, status_code=201) + returned = self.manager.create(**request_args) + self.assertEqual(expected, returned.to_dict()) + request_body = {'mapping_id': request_args['mapping']} + self.assertEntityRequestBodyIs(request_body) + + def test_get(self): + """Fetch federation protocol object. + + URL to be tested: GET /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + """ + request_args = self.new_ref() + expected = self._transform_to_response(request_args) + + parts = self.build_parts(request_args['identity_provider'], + request_args['protocol_id']) + self.stub_entity('GET', entity=expected, + parts=parts, status_code=201) + + returned = self.manager.get(request_args['identity_provider'], + request_args['protocol_id']) + self.assertIsInstance(returned, self.model) + self.assertEqual(expected, returned.to_dict()) + + def test_delete(self): + """Delete federation protocol object. + + URL to be tested: DELETE /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + """ + request_args = self.new_ref() + parts = self.build_parts(request_args['identity_provider'], + request_args['protocol_id']) + + self.stub_entity('DELETE', parts=parts, status_code=204) + + self.manager.delete(request_args['identity_provider'], + request_args['protocol_id']) + + def test_list(self): + """Test listing all federation protocols tied to the Identity Provider. + + URL to be tested: GET /OS-FEDERATION/identity_providers/ + $identity_provider/protocols + + """ + def _ref_protocols(): + return { + 'id': uuid.uuid4().hex, + 'mapping_id': uuid.uuid4().hex + } + + request_args = self.new_ref() + expected = [_ref_protocols() for _ in range(3)] + parts = self.build_parts(request_args['identity_provider']) + self.stub_entity('GET', parts=parts, + entity=expected, status_code=200) + + returned = self.manager.list(request_args['identity_provider']) + for obj, ref_obj in zip(returned, expected): + self.assertEqual(obj.to_dict(), ref_obj) + + def test_list_params(self): + request_args = self.new_ref() + filter_kwargs = {uuid.uuid4().hex: uuid.uuid4().hex} + parts = self.build_parts(request_args['identity_provider']) + + # Return HTTP 401 as we don't accept such requests. + self.stub_entity('GET', parts=parts, status_code=401) + self.assertRaises(exceptions.Unauthorized, + self.manager.list, + request_args['identity_provider'], + **filter_kwargs) + self.assertQueryStringContains(**filter_kwargs) + + def test_update(self): + """Test updating federation protocol + + URL to be tested: PATCH /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + """ + request_args = self.new_ref() + expected = self._transform_to_response(request_args) + + parts = self.build_parts(request_args['identity_provider'], + request_args['protocol_id']) + + self.stub_entity('PATCH', parts=parts, + entity=expected, status_code=200) + + returned = self.manager.update(request_args['identity_provider'], + request_args['protocol_id'], + mapping=request_args['mapping']) + self.assertIsInstance(returned, self.model) + self.assertEqual(expected, returned.to_dict()) + request_body = {'mapping_id': request_args['mapping']} + self.assertEntityRequestBodyIs(request_body) + + +class EntityManagerTests(utils.TestCase): + def test_create_object_expect_fail(self): + self.assertRaises(TypeError, + base.EntityManager, + self.client) + + +class FederationProjectTests(utils.TestCase): + + def setUp(self): + super(FederationProjectTests, self).setUp() + self.key = 'project' + self.collection_key = 'projects' + self.model = projects.Project + self.manager = self.client.federation.projects + self.URL = "%s%s" % (self.TEST_URL, '/OS-FEDERATION/projects') + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_list_accessible_projects(self): + projects_ref = [self.new_ref(), self.new_ref()] + projects_json = { + self.collection_key: [self.new_ref(), self.new_ref()] + } + self.requests.get(self.URL, json=projects_json) + returned_list = self.manager.list() + + self.assertEqual(len(projects_ref), len(returned_list)) + for project in returned_list: + self.assertIsInstance(project, self.model) + + +class FederationDomainTests(utils.TestCase): + + def setUp(self): + super(FederationDomainTests, self).setUp() + self.key = 'domain' + self.collection_key = 'domains' + self.model = domains.Domain + self.manager = self.client.federation.domains + + self.URL = "%s%s" % (self.TEST_URL, '/OS-FEDERATION/domains') + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + return kwargs + + def test_list_accessible_domains(self): + domains_ref = [self.new_ref(), self.new_ref()] + domains_json = { + self.collection_key: domains_ref + } + self.requests.get(self.URL, json=domains_json) + returned_list = self.manager.list() + self.assertEqual(len(domains_ref), len(returned_list)) + for domain in returned_list: + self.assertIsInstance(domain, self.model) + + +class FederatedTokenTests(utils.TestCase): + + def setUp(self): + super(FederatedTokenTests, self).setUp() + token = fixture.V3FederationToken() + token.set_project_scope() + token.add_role() + self.federated_token = access.AccessInfo.factory(body=token) + + def test_federated_property_federated_token(self): + """Check if is_federated property returns expected value.""" + self.assertTrue(self.federated_token.is_federated) + + def test_get_user_domain_name(self): + """Ensure a federated user's domain name does not exist.""" + self.assertIsNone(self.federated_token.user_domain_name) + + def test_get_user_domain_id(self): + """Ensure a federated user's domain ID does not exist.""" + self.assertIsNone(self.federated_token.user_domain_id) diff --git a/keystoneclient/tests/unit/v3/test_groups.py b/keystoneclient/tests/unit/v3/test_groups.py new file mode 100644 index 0000000..6ed140c --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_groups.py @@ -0,0 +1,58 @@ +# Copyright 2012 OpenStack Foundation +# +# 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.tests.unit.v3 import utils +from keystoneclient.v3 import groups + + +class GroupTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(GroupTests, self).setUp() + self.key = 'group' + self.collection_key = 'groups' + self.model = groups.Group + self.manager = self.client.groups + + def new_ref(self, **kwargs): + kwargs = super(GroupTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_list_groups_for_user(self): + user_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['users', user_id, self.collection_key], + status_code=200, entity=ref_list) + + returned_list = self.manager.list(user=user_id) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_list_groups_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + domain_id = uuid.uuid4().hex + + self.stub_entity('GET', + [self.collection_key], + status_code=200, entity=ref_list) + + returned_list = self.manager.list(domain=domain_id) + self.assertTrue(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + self.assertQueryStringIs('domain_id=%s' % domain_id) diff --git a/keystoneclient/tests/unit/v3/test_oauth1.py b/keystoneclient/tests/unit/v3/test_oauth1.py new file mode 100644 index 0000000..d259053 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_oauth1.py @@ -0,0 +1,298 @@ +# 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 + +import mock +from oslo_utils import timeutils +import six +from six.moves.urllib import parse as urlparse +from testtools import matchers + +from keystoneclient import session +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib.oauth1 import access_tokens +from keystoneclient.v3.contrib.oauth1 import auth +from keystoneclient.v3.contrib.oauth1 import consumers +from keystoneclient.v3.contrib.oauth1 import request_tokens + +try: + import oauthlib + from oauthlib import oauth1 +except ImportError: + oauth1 = None + + +class BaseTest(utils.TestCase): + def setUp(self): + super(BaseTest, self).setUp() + if oauth1 is None: + self.skipTest('oauthlib package not available') + + +class ConsumerTests(BaseTest, utils.CrudTests): + def setUp(self): + super(ConsumerTests, self).setUp() + self.key = 'consumer' + self.collection_key = 'consumers' + self.model = consumers.Consumer + self.manager = self.client.oauth1.consumers + self.path_prefix = 'OS-OAUTH1' + + def new_ref(self, **kwargs): + kwargs = super(ConsumerTests, self).new_ref(**kwargs) + kwargs.setdefault('description', uuid.uuid4().hex) + return kwargs + + def test_description_is_optional(self): + consumer_id = uuid.uuid4().hex + resp_ref = {'consumer': {'description': None, + 'id': consumer_id}} + + self.stub_url('POST', + [self.path_prefix, self.collection_key], + status_code=201, json=resp_ref) + + consumer = self.manager.create() + self.assertEqual(consumer_id, consumer.id) + self.assertIsNone(consumer.description) + + def test_description_not_included(self): + consumer_id = uuid.uuid4().hex + resp_ref = {'consumer': {'id': consumer_id}} + + self.stub_url('POST', + [self.path_prefix, self.collection_key], + status_code=201, json=resp_ref) + + consumer = self.manager.create() + self.assertEqual(consumer_id, consumer.id) + + +class TokenTests(BaseTest): + def _new_oauth_token(self): + key = uuid.uuid4().hex + secret = uuid.uuid4().hex + params = {'oauth_token': key, 'oauth_token_secret': secret} + token = urlparse.urlencode(params) + return (key, secret, token) + + def _new_oauth_token_with_expires_at(self): + key, secret, token = self._new_oauth_token() + expires_at = timeutils.strtime() + params = {'oauth_token': key, + 'oauth_token_secret': secret, + 'oauth_expires_at': expires_at} + token = urlparse.urlencode(params) + return (key, secret, expires_at, token) + + def _validate_oauth_headers(self, auth_header, oauth_client): + """Assert that the data in the headers matches the data + that is produced from oauthlib. + """ + + self.assertThat(auth_header, matchers.StartsWith('OAuth ')) + auth_header = auth_header[len('OAuth '):] + # NOTE(stevemar): In newer versions of oauthlib there is + # an additional argument for getting oauth parameters. + # Adding a conditional here to revert back to no arguments + # if an earlier version is detected. + if tuple(oauthlib.__version__.split('.')) > ('0', '6', '1'): + header_params = oauth_client.get_oauth_params(None) + else: + header_params = oauth_client.get_oauth_params() + parameters = dict(header_params) + + self.assertEqual('HMAC-SHA1', parameters['oauth_signature_method']) + self.assertEqual('1.0', parameters['oauth_version']) + self.assertIsInstance(parameters['oauth_nonce'], six.string_types) + self.assertEqual(oauth_client.client_key, + parameters['oauth_consumer_key']) + if oauth_client.resource_owner_key: + self.assertEqual(oauth_client.resource_owner_key, + parameters['oauth_token'],) + if oauth_client.verifier: + self.assertEqual(oauth_client.verifier, + parameters['oauth_verifier']) + if oauth_client.callback_uri: + self.assertEqual(oauth_client.callback_uri, + parameters['oauth_callback']) + if oauth_client.timestamp: + self.assertEqual(oauth_client.timestamp, + parameters['oauth_timestamp']) + return parameters + + +class RequestTokenTests(TokenTests): + def setUp(self): + super(RequestTokenTests, self).setUp() + self.model = request_tokens.RequestToken + self.manager = self.client.oauth1.request_tokens + self.path_prefix = 'OS-OAUTH1' + + def test_authorize_request_token(self): + request_key = uuid.uuid4().hex + info = {'id': request_key, + 'key': request_key, + 'secret': uuid.uuid4().hex} + request_token = request_tokens.RequestToken(self.manager, info) + + verifier = uuid.uuid4().hex + resp_ref = {'token': {'oauth_verifier': verifier}} + self.stub_url('PUT', + [self.path_prefix, 'authorize', request_key], + status_code=200, json=resp_ref) + + # Assert the manager is returning the expected data + role_id = uuid.uuid4().hex + token = request_token.authorize([role_id]) + self.assertEqual(verifier, token.oauth_verifier) + + # Assert that the request was sent in the expected structure + exp_body = {'roles': [{'id': role_id}]} + self.assertRequestBodyIs(json=exp_body) + + def test_create_request_token(self): + project_id = uuid.uuid4().hex + consumer_key = uuid.uuid4().hex + consumer_secret = uuid.uuid4().hex + + request_key, request_secret, resp_ref = self._new_oauth_token() + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + self.stub_url('POST', [self.path_prefix, 'request_token'], + status_code=201, text=resp_ref, headers=headers) + + # Assert the manager is returning request token object + request_token = self.manager.create(consumer_key, consumer_secret, + project_id) + self.assertIsInstance(request_token, self.model) + self.assertEqual(request_key, request_token.key) + self.assertEqual(request_secret, request_token.secret) + + # Assert that the project id is in the header + self.assertRequestHeaderEqual('requested-project-id', project_id) + req_headers = self.requests.last_request.headers + + oauth_client = oauth1.Client(consumer_key, + client_secret=consumer_secret, + signature_method=oauth1.SIGNATURE_HMAC, + callback_uri="oob") + self._validate_oauth_headers(req_headers['Authorization'], + oauth_client) + + +class AccessTokenTests(TokenTests): + def setUp(self): + super(AccessTokenTests, self).setUp() + self.manager = self.client.oauth1.access_tokens + self.model = access_tokens.AccessToken + self.path_prefix = 'OS-OAUTH1' + + def test_create_access_token_expires_at(self): + verifier = uuid.uuid4().hex + consumer_key = uuid.uuid4().hex + consumer_secret = uuid.uuid4().hex + request_key = uuid.uuid4().hex + request_secret = uuid.uuid4().hex + + t = self._new_oauth_token_with_expires_at() + access_key, access_secret, expires_at, resp_ref = t + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + self.stub_url('POST', [self.path_prefix, 'access_token'], + status_code=201, text=resp_ref, headers=headers) + + # Assert that the manager creates an access token object + access_token = self.manager.create(consumer_key, consumer_secret, + request_key, request_secret, + verifier) + self.assertIsInstance(access_token, self.model) + self.assertEqual(access_key, access_token.key) + self.assertEqual(access_secret, access_token.secret) + self.assertEqual(expires_at, access_token.expires) + + req_headers = self.requests.last_request.headers + oauth_client = oauth1.Client(consumer_key, + client_secret=consumer_secret, + resource_owner_key=request_key, + resource_owner_secret=request_secret, + signature_method=oauth1.SIGNATURE_HMAC, + verifier=verifier, + timestamp=expires_at) + self._validate_oauth_headers(req_headers['Authorization'], + oauth_client) + + +class AuthenticateWithOAuthTests(TokenTests): + def setUp(self): + super(AuthenticateWithOAuthTests, self).setUp() + if oauth1 is None: + self.skipTest('optional package oauthlib is not installed') + + def test_oauth_authenticate_success(self): + consumer_key = uuid.uuid4().hex + consumer_secret = uuid.uuid4().hex + access_key = uuid.uuid4().hex + access_secret = uuid.uuid4().hex + + # Just use an existing project scoped token and change + # the methods to oauth1, and add an OS-OAUTH1 section. + oauth_token = client_fixtures.project_scoped_token() + oauth_token['methods'] = ["oauth1"] + oauth_token['OS-OAUTH1'] = {"consumer_id": consumer_key, + "access_token_id": access_key} + self.stub_auth(json=oauth_token) + + a = auth.OAuth(self.TEST_URL, consumer_key=consumer_key, + consumer_secret=consumer_secret, + access_key=access_key, + access_secret=access_secret) + s = session.Session(auth=a) + t = s.get_token() + self.assertEqual(self.TEST_TOKEN, t) + + OAUTH_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["oauth1"], + "oauth1": {} + } + } + } + + self.assertRequestBodyIs(json=OAUTH_REQUEST_BODY) + + # Assert that the headers have the same oauthlib data + req_headers = self.requests.last_request.headers + oauth_client = oauth1.Client(consumer_key, + client_secret=consumer_secret, + resource_owner_key=access_key, + resource_owner_secret=access_secret, + signature_method=oauth1.SIGNATURE_HMAC) + self._validate_oauth_headers(req_headers['Authorization'], + oauth_client) + + +class TestOAuthLibModule(utils.TestCase): + + def test_no_oauthlib_installed(self): + with mock.patch.object(auth, 'oauth1', None): + self.assertRaises(NotImplementedError, + auth.OAuth, + self.TEST_URL, + consumer_key=uuid.uuid4().hex, + consumer_secret=uuid.uuid4().hex, + access_key=uuid.uuid4().hex, + access_secret=uuid.uuid4().hex) diff --git a/keystoneclient/tests/unit/v3/test_policies.py b/keystoneclient/tests/unit/v3/test_policies.py new file mode 100644 index 0000000..a7f8d8a --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_policies.py @@ -0,0 +1,31 @@ +# 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.tests.unit.v3 import utils +from keystoneclient.v3 import policies + + +class PolicyTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(PolicyTests, self).setUp() + self.key = 'policy' + self.collection_key = 'policies' + self.model = policies.Policy + self.manager = self.client.policies + + def new_ref(self, **kwargs): + kwargs = super(PolicyTests, self).new_ref(**kwargs) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('blob', uuid.uuid4().hex) + return kwargs diff --git a/keystoneclient/tests/unit/v3/test_projects.py b/keystoneclient/tests/unit/v3/test_projects.py new file mode 100644 index 0000000..5d08bb2 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_projects.py @@ -0,0 +1,227 @@ +# 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 import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import projects + + +class ProjectTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ProjectTests, self).setUp() + self.key = 'project' + self.collection_key = 'projects' + self.model = projects.Project + self.manager = self.client.projects + + def new_ref(self, **kwargs): + kwargs = super(ProjectTests, self).new_ref(**kwargs) + return self._new_project_ref(ref=kwargs) + + def _new_project_ref(self, ref=None): + ref = ref or {} + ref.setdefault('domain_id', uuid.uuid4().hex) + ref.setdefault('enabled', True) + ref.setdefault('name', uuid.uuid4().hex) + return ref + + def test_list_projects_for_user(self): + ref_list = [self.new_ref(), self.new_ref()] + user_id = uuid.uuid4().hex + + self.stub_entity('GET', + ['users', user_id, self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(user=user_id) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_list_projects_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + domain_id = uuid.uuid4().hex + + self.stub_entity('GET', [self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(domain=domain_id) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + self.assertQueryStringIs('domain_id=%s' % domain_id) + + def test_create_with_parent(self): + parent_ref = self.new_ref() + parent_ref['parent_id'] = uuid.uuid4().hex + parent = self.test_create(ref=parent_ref) + parent.id = parent_ref['id'] + + # Create another project under 'parent' in the hierarchy + ref = self.new_ref() + ref['parent_id'] = parent.id + + child_ref = ref.copy() + del child_ref['parent_id'] + child_ref['parent'] = parent + + # test_create() pops the 'id' of the mocked response + del ref['id'] + + # Resource objects may peform lazy-loading. The create() method of + # ProjectManager will try to access the 'uuid' attribute of the parent + # object, which will trigger a call to fetch the Resource attributes. + self.stub_entity('GET', id=parent_ref['id'], entity=parent_ref) + self.test_create(ref=child_ref, req_ref=ref) + + def test_create_with_parent_id(self): + ref = self._new_project_ref() + ref['parent_id'] = uuid.uuid4().hex + + self.stub_entity('POST', entity=ref, status_code=201) + + returned = self.manager.create(name=ref['name'], + domain=ref['domain_id'], + parent_id=ref['parent_id']) + + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(ref) + + def test_create_with_parent_and_parent_id(self): + ref = self._new_project_ref() + ref['parent_id'] = uuid.uuid4().hex + + self.stub_entity('POST', entity=ref, status_code=201) + + # Should ignore the 'parent_id' argument since we are also passing + # 'parent' + returned = self.manager.create(name=ref['name'], + domain=ref['domain_id'], + parent=ref['parent_id'], + parent_id=uuid.uuid4().hex) + + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(ref) + + def _create_projects_hierarchy(self, hierarchy_size=3): + """Creates a project hierarchy with specified size. + + :param hierarchy_size: the desired hierarchy size, default is 3. + + :returns: a list of the projects in the created hierarchy. + + """ + + ref = self.new_ref() + project_id = ref['id'] + projects = [ref] + + for i in range(1, hierarchy_size): + new_ref = self.new_ref() + new_ref['parent_id'] = project_id + projects.append(new_ref) + project_id = new_ref['id'] + + return projects + + def test_get_with_subtree_as_list(self): + projects = self._create_projects_hierarchy() + ref = projects[0] + + ref['subtree_as_list'] = [] + for i in range(1, len(projects)): + ref['subtree_as_list'].append(projects[i]) + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], subtree_as_list=True) + self.assertQueryStringIs('subtree_as_list') + for i in range(1, len(projects)): + for attr in projects[i]: + child = getattr(returned, 'subtree_as_list')[i - 1] + self.assertEqual( + child[attr], + projects[i][attr], + 'Expected different %s' % attr) + + def test_get_with_parents_as_list(self): + projects = self._create_projects_hierarchy() + ref = projects[2] + + ref['parents_as_list'] = [] + for i in range(0, len(projects) - 1): + ref['parents_as_list'].append(projects[i]) + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], parents_as_list=True) + self.assertQueryStringIs('parents_as_list') + for i in range(0, len(projects) - 1): + for attr in projects[i]: + parent = getattr(returned, 'parents_as_list')[i] + self.assertEqual( + parent[attr], + projects[i][attr], + 'Expected different %s' % attr) + + def test_get_with_parents_as_list_and_subtree_as_list(self): + ref = self.new_ref() + projects = self._create_projects_hierarchy() + ref = projects[1] + + ref['parents_as_list'] = [projects[0]] + ref['subtree_as_list'] = [projects[2]] + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], + parents_as_list=True, + subtree_as_list=True) + self.assertQueryStringIs('subtree_as_list&parents_as_list') + + for attr in projects[0]: + parent = getattr(returned, 'parents_as_list')[0] + self.assertEqual( + parent[attr], + projects[0][attr], + 'Expected different %s' % attr) + + for attr in projects[2]: + child = getattr(returned, 'subtree_as_list')[0] + self.assertEqual( + child[attr], + projects[2][attr], + 'Expected different %s' % attr) + + def test_update_with_parent_project(self): + ref = self.new_ref() + ref['parent_id'] = uuid.uuid4().hex + + self.stub_entity('PATCH', id=ref['id'], entity=ref, status_code=403) + req_ref = ref.copy() + req_ref.pop('id') + + # NOTE(rodrigods): this is the expected behaviour of the Identity + # server, a different implementation might not fail this request. + self.assertRaises(exceptions.Forbidden, self.manager.update, + ref['id'], **utils.parameterize(req_ref)) diff --git a/keystoneclient/tests/unit/v3/test_regions.py b/keystoneclient/tests/unit/v3/test_regions.py new file mode 100644 index 0000000..392f79f --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_regions.py @@ -0,0 +1,37 @@ +# 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.tests.unit.v3 import utils +from keystoneclient.v3 import regions + + +class RegionTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(RegionTests, self).setUp() + self.key = 'region' + self.collection_key = 'regions' + self.model = regions.Region + self.manager = self.client.regions + + def new_ref(self, **kwargs): + kwargs = super(RegionTests, self).new_ref(**kwargs) + kwargs.setdefault('enabled', True) + kwargs.setdefault('id', uuid.uuid4().hex) + return kwargs + + def test_update_enabled_defaults_to_none(self): + super(RegionTests, self).test_update( + req_ref={'description': uuid.uuid4().hex}) diff --git a/keystoneclient/tests/unit/v3/test_role_assignments.py b/keystoneclient/tests/unit/v3/test_role_assignments.py new file mode 100644 index 0000000..1a664c9 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_role_assignments.py @@ -0,0 +1,210 @@ +# 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 import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import role_assignments + + +class RoleAssignmentsTests(utils.TestCase, utils.CrudTests): + + def setUp(self): + super(RoleAssignmentsTests, self).setUp() + self.key = 'role_assignment' + self.collection_key = 'role_assignments' + self.model = role_assignments.RoleAssignment + self.manager = self.client.role_assignments + self.TEST_USER_DOMAIN_LIST = [{ + 'role': { + 'id': self.TEST_ROLE_ID + }, + 'scope': { + 'domain': { + 'id': self.TEST_DOMAIN_ID + } + }, + 'user': { + 'id': self.TEST_USER_ID + } + }] + self.TEST_GROUP_PROJECT_LIST = [{ + 'group': { + 'id': self.TEST_GROUP_ID + }, + 'role': { + 'id': self.TEST_ROLE_ID + }, + 'scope': { + 'project': { + 'id': self.TEST_TENANT_ID + } + } + }] + self.TEST_USER_PROJECT_LIST = [{ + 'user': { + 'id': self.TEST_USER_ID + }, + 'role': { + 'id': self.TEST_ROLE_ID + }, + 'scope': { + 'project': { + 'id': self.TEST_TENANT_ID + } + } + }] + + self.TEST_ALL_RESPONSE_LIST = (self.TEST_USER_PROJECT_LIST + + self.TEST_GROUP_PROJECT_LIST + + self.TEST_USER_DOMAIN_LIST) + + def _assert_returned_list(self, ref_list, returned_list): + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_list_params(self): + ref_list = self.TEST_USER_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?scope.project.id=%s&user.id=%s' % + (self.TEST_TENANT_ID, self.TEST_USER_ID)], + entity=ref_list) + + returned_list = self.manager.list(user=self.TEST_USER_ID, + project=self.TEST_TENANT_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.project.id': self.TEST_TENANT_ID, + 'user.id': self.TEST_USER_ID} + self.assertQueryStringContains(**kwargs) + + def test_all_assignments_list(self): + ref_list = self.TEST_ALL_RESPONSE_LIST + self.stub_entity('GET', + [self.collection_key], + entity=ref_list) + + returned_list = self.manager.list() + self._assert_returned_list(ref_list, returned_list) + + kwargs = {} + self.assertQueryStringContains(**kwargs) + + def test_project_assignments_list(self): + ref_list = self.TEST_GROUP_PROJECT_LIST + self.TEST_USER_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?scope.project.id=%s' % self.TEST_TENANT_ID], + entity=ref_list) + + returned_list = self.manager.list(project=self.TEST_TENANT_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.project.id': self.TEST_TENANT_ID} + self.assertQueryStringContains(**kwargs) + + def test_domain_assignments_list(self): + ref_list = self.TEST_USER_DOMAIN_LIST + self.stub_entity('GET', + [self.collection_key, + '?scope.domain.id=%s' % self.TEST_DOMAIN_ID], + entity=ref_list) + + returned_list = self.manager.list(domain=self.TEST_DOMAIN_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.domain.id': self.TEST_DOMAIN_ID} + self.assertQueryStringContains(**kwargs) + + def test_group_assignments_list(self): + ref_list = self.TEST_GROUP_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?group.id=%s' % self.TEST_GROUP_ID], + entity=ref_list) + + returned_list = self.manager.list(group=self.TEST_GROUP_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'group.id': self.TEST_GROUP_ID} + self.assertQueryStringContains(**kwargs) + + def test_user_assignments_list(self): + ref_list = self.TEST_USER_DOMAIN_LIST + self.TEST_USER_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?user.id=%s' % self.TEST_USER_ID], + entity=ref_list) + + returned_list = self.manager.list(user=self.TEST_USER_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'user.id': self.TEST_USER_ID} + self.assertQueryStringContains(**kwargs) + + def test_effective_assignments_list(self): + ref_list = self.TEST_USER_PROJECT_LIST + self.TEST_USER_DOMAIN_LIST + self.stub_entity('GET', + [self.collection_key, + '?effective=True'], + entity=ref_list) + + returned_list = self.manager.list(effective=True) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'effective': 'True'} + self.assertQueryStringContains(**kwargs) + + def test_role_assignments_list(self): + ref_list = self.TEST_ALL_RESPONSE_LIST + self.stub_entity('GET', + [self.collection_key, + '?role.id=' + self.TEST_ROLE_ID], + entity=ref_list) + + returned_list = self.manager.list(role=self.TEST_ROLE_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'role.id': self.TEST_ROLE_ID} + self.assertQueryStringContains(**kwargs) + + def test_domain_and_project_list(self): + # Should only accept either domain or project, never both + self.assertRaises(exceptions.ValidationError, + self.manager.list, + domain=self.TEST_DOMAIN_ID, + project=self.TEST_TENANT_ID) + + def test_user_and_group_list(self): + # Should only accept either user or group, never both + self.assertRaises(exceptions.ValidationError, self.manager.list, + user=self.TEST_USER_ID, group=self.TEST_GROUP_ID) + + def test_create(self): + # Create not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.create) + + def test_update(self): + # Update not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) + + def test_delete(self): + # Delete not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.delete) + + def test_get(self): + # Get not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.get) + + def test_find(self): + # Find not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.find) diff --git a/keystoneclient/tests/unit/v3/test_roles.py b/keystoneclient/tests/unit/v3/test_roles.py new file mode 100644 index 0000000..2a71bf3 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_roles.py @@ -0,0 +1,331 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import roles + + +class RoleTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(RoleTests, self).setUp() + self.key = 'role' + self.collection_key = 'roles' + self.model = roles.Role + self.manager = self.client.roles + + def new_ref(self, **kwargs): + kwargs = super(RoleTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_domain_role_grant(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=201) + + self.manager.grant(role=ref['id'], domain=domain_id, user=user_id) + + def test_domain_group_role_grant(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=201) + + self.manager.grant(role=ref['id'], domain=domain_id, group=group_id) + + def test_domain_role_list(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['domains', domain_id, 'users', user_id, + self.collection_key], entity=ref_list) + + self.manager.list(domain=domain_id, user=user_id) + + def test_domain_group_role_list(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['domains', domain_id, 'groups', group_id, + self.collection_key], entity=ref_list) + + self.manager.list(domain=domain_id, group=group_id) + + def test_domain_role_check(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.check(role=ref['id'], domain=domain_id, + user=user_id) + + def test_domain_group_role_check(self): + return + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.check(role=ref['id'], domain=domain_id, group=group_id) + + def test_domain_role_revoke(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, user=user_id) + + def test_domain_group_role_revoke(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, group=group_id) + + def test_project_role_grant(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=201) + + self.manager.grant(role=ref['id'], project=project_id, user=user_id) + + def test_project_group_role_grant(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=201) + + self.manager.grant(role=ref['id'], project=project_id, group=group_id) + + def test_project_role_list(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['projects', project_id, 'users', user_id, + self.collection_key], entity=ref_list) + + self.manager.list(project=project_id, user=user_id) + + def test_project_group_role_list(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['projects', project_id, 'groups', group_id, + self.collection_key], entity=ref_list) + + self.manager.list(project=project_id, group=group_id) + + def test_project_role_check(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=200) + + self.manager.check(role=ref['id'], project=project_id, user=user_id) + + def test_project_group_role_check(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=200) + + self.manager.check(role=ref['id'], project=project_id, group=group_id) + + def test_project_role_revoke(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.revoke(role=ref['id'], project=project_id, user=user_id) + + def test_project_group_role_revoke(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.revoke(role=ref['id'], project=project_id, group=group_id) + + def test_domain_project_role_grant_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.grant, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_list_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + + self.assertRaises( + exceptions.ValidationError, + self.manager.list, + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_check_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.check, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_revoke_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.revoke, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_user_group_role_grant_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.grant, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_list_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.assertRaises( + exceptions.ValidationError, + self.manager.list, + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_check_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.check, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_revoke_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.revoke, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) diff --git a/keystoneclient/tests/unit/v3/test_service_catalog.py b/keystoneclient/tests/unit/v3/test_service_catalog.py new file mode 100644 index 0000000..a187302 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_service_catalog.py @@ -0,0 +1,218 @@ +# 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 import access +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils + + +class ServiceCatalogTest(utils.TestCase): + def setUp(self): + super(ServiceCatalogTest, self).setUp() + self.AUTH_RESPONSE_BODY = client_fixtures.auth_response_body() + self.RESPONSE = utils.TestResponse({ + "headers": client_fixtures.AUTH_RESPONSE_HEADERS + }) + + self.north_endpoints = {'public': + 'http://glance.north.host/glanceapi/public', + 'internal': + 'http://glance.north.host/glanceapi/internal', + 'admin': + 'http://glance.north.host/glanceapi/admin'} + + self.south_endpoints = {'public': + 'http://glance.south.host/glanceapi/public', + 'internal': + 'http://glance.south.host/glanceapi/internal', + 'admin': + 'http://glance.south.host/glanceapi/admin'} + + def test_building_a_service_catalog(self): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + self.assertEqual(sc.url_for(service_type='compute'), + "https://compute.north.host/novapi/public") + self.assertEqual(sc.url_for(service_type='compute', + endpoint_type='internal'), + "https://compute.north.host/novapi/internal") + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", + "South", service_type='compute') + + def test_service_catalog_endpoints(self): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + public_ep = sc.get_endpoints(service_type='compute', + endpoint_type='public') + self.assertEqual(public_ep['compute'][0]['region'], 'North') + self.assertEqual(public_ep['compute'][0]['url'], + "https://compute.north.host/novapi/public") + + def test_service_catalog_regions(self): + self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='public') + self.assertEqual(url, "http://glance.north.host/glanceapi/public") + + self.AUTH_RESPONSE_BODY['token']['region_name'] = "South" + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + url = sc.url_for(service_type='image', endpoint_type='internal') + self.assertEqual(url, "http://glance.south.host/glanceapi/internal") + + def test_service_catalog_empty(self): + self.AUTH_RESPONSE_BODY['token']['catalog'] = [] + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + self.assertRaises(exceptions.EmptyCatalog, + auth_ref.service_catalog.url_for, + service_type='image', + endpoint_type='internalURL') + + def test_service_catalog_get_endpoints_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + endpoints = sc.get_endpoints(service_type='image', region_name='North') + self.assertEqual(len(endpoints), 1) + for endpoint in endpoints['image']: + self.assertEqual(endpoint['url'], + self.north_endpoints[endpoint['interface']]) + + endpoints = sc.get_endpoints(service_type='image', region_name='South') + self.assertEqual(len(endpoints), 1) + for endpoint in endpoints['image']: + self.assertEqual(endpoint['url'], + self.south_endpoints[endpoint['interface']]) + + endpoints = sc.get_endpoints(service_type='compute') + self.assertEqual(len(endpoints['compute']), 3) + + endpoints = sc.get_endpoints(service_type='compute', + region_name='North') + self.assertEqual(len(endpoints['compute']), 3) + + endpoints = sc.get_endpoints(service_type='compute', + region_name='West') + self.assertEqual(len(endpoints['compute']), 0) + + def test_service_catalog_url_for_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', region_name='North') + self.assertEqual(url, self.north_endpoints['public']) + + url = sc.url_for(service_type='image', region_name='South') + self.assertEqual(url, self.south_endpoints['public']) + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + service_type='image', region_name='West') + + def test_servcie_catalog_get_url_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + urls = sc.get_urls(service_type='image') + self.assertEqual(len(urls), 2) + + urls = sc.get_urls(service_type='image', region_name='North') + self.assertEqual(len(urls), 1) + self.assertEqual(urls[0], self.north_endpoints['public']) + + urls = sc.get_urls(service_type='image', region_name='South') + self.assertEqual(len(urls), 1) + self.assertEqual(urls[0], self.south_endpoints['public']) + + urls = sc.get_urls(service_type='image', region_name='West') + self.assertIsNone(urls) + + def test_service_catalog_param_overrides_body_region(self): + self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image') + self.assertEqual(url, self.north_endpoints['public']) + + url = sc.url_for(service_type='image', region_name='South') + self.assertEqual(url, self.south_endpoints['public']) + + endpoints = sc.get_endpoints(service_type='image') + self.assertEqual(len(endpoints['image']), 3) + for endpoint in endpoints['image']: + self.assertEqual(endpoint['url'], + self.north_endpoints[endpoint['interface']]) + + endpoints = sc.get_endpoints(service_type='image', region_name='South') + self.assertEqual(len(endpoints['image']), 3) + for endpoint in endpoints['image']: + self.assertEqual(endpoint['url'], + self.south_endpoints[endpoint['interface']]) + + def test_service_catalog_service_name(self): + auth_ref = access.AccessInfo.factory(resp=None, + body=self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_name='glance', endpoint_type='public', + service_type='image', region_name='North') + self.assertEqual('http://glance.north.host/glanceapi/public', url) + + url = sc.url_for(service_name='glance', endpoint_type='public', + service_type='image', region_name='South') + self.assertEqual('http://glance.south.host/glanceapi/public', url) + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + service_name='glance', service_type='compute') + + urls = sc.get_urls(service_type='image', service_name='glance', + endpoint_type='public') + + self.assertIn('http://glance.north.host/glanceapi/public', urls) + self.assertIn('http://glance.south.host/glanceapi/public', urls) + + urls = sc.get_urls(service_type='image', service_name='Servers', + endpoint_type='public') + + self.assertIsNone(urls) + + def test_service_catalog_without_name(self): + pr_auth_ref = access.AccessInfo.factory( + resp=None, + body=client_fixtures.project_scoped_token()) + pr_sc = pr_auth_ref.service_catalog + + # this will work because there are no service names on that token + url_ref = 'http://public.com:8774/v2/225da22d3ce34b15877ea70b2a575f58' + url = pr_sc.url_for(service_type='compute', service_name='NotExist', + endpoint_type='public') + self.assertEqual(url_ref, url) + + ab_auth_ref = access.AccessInfo.factory(resp=None, + body=self.AUTH_RESPONSE_BODY) + ab_sc = ab_auth_ref.service_catalog + + # this won't work because there is a name and it's not this one + self.assertRaises(exceptions.EndpointNotFound, ab_sc.url_for, + service_type='compute', service_name='NotExist', + endpoint_type='public') diff --git a/keystoneclient/tests/unit/v3/test_services.py b/keystoneclient/tests/unit/v3/test_services.py new file mode 100644 index 0000000..40dde47 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_services.py @@ -0,0 +1,44 @@ +# 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.tests.unit.v3 import utils +from keystoneclient.v3 import services + + +class ServiceTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ServiceTests, self).setUp() + self.key = 'service' + self.collection_key = 'services' + self.model = services.Service + self.manager = self.client.services + + def new_ref(self, **kwargs): + kwargs = super(ServiceTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def test_list_filter_name(self): + filter_name = uuid.uuid4().hex + expected_query = {'name': filter_name} + super(ServiceTests, self).test_list(expected_query=expected_query, + name=filter_name) + + def test_list_filter_type(self): + filter_type = uuid.uuid4().hex + expected_query = {'type': filter_type} + super(ServiceTests, self).test_list(expected_query=expected_query, + type=filter_type) diff --git a/keystoneclient/tests/unit/v3/test_tokens.py b/keystoneclient/tests/unit/v3/test_tokens.py new file mode 100644 index 0000000..2c27fd0 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_tokens.py @@ -0,0 +1,110 @@ +# 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 + +import testresources + +from keystoneclient import access +from keystoneclient import exceptions +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit.v3 import utils + + +class TokenTests(utils.TestCase, testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_revoke_token_with_token_id(self): + token_id = uuid.uuid4().hex + self.stub_url('DELETE', ['/auth/tokens'], status_code=204) + self.client.tokens.revoke_token(token_id) + self.assertRequestHeaderEqual('X-Subject-Token', token_id) + + def test_revoke_token_with_access_info_instance(self): + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_DEFAULT] + token = access.AccessInfoV3(token_id, token_ref['token']) + self.stub_url('DELETE', ['/auth/tokens'], status_code=204) + self.client.tokens.revoke_token(token) + self.assertRequestHeaderEqual('X-Subject-Token', token_id) + + def test_get_revoked(self): + sample_revoked_response = {'signed': '-----BEGIN CMS-----\nMIIB...'} + self.stub_url('GET', ['auth', 'tokens', 'OS-PKI', 'revoked'], + json=sample_revoked_response) + resp = self.client.tokens.get_revoked() + self.assertEqual(sample_revoked_response, resp) + + def test_validate_token_with_token_id(self): + # Can validate a token passing a string token ID. + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_DEFAULT] + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + access_info = self.client.tokens.validate(token_id) + + self.assertRequestHeaderEqual('X-Subject-Token', token_id) + self.assertIsInstance(access_info, access.AccessInfoV3) + self.assertEqual(token_id, access_info.auth_token) + + def test_validate_token_with_access_info(self): + # Can validate a token passing an access info. + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_DEFAULT] + token = access.AccessInfoV3(token_id, token_ref['token']) + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + access_info = self.client.tokens.validate(token) + + self.assertRequestHeaderEqual('X-Subject-Token', token_id) + self.assertIsInstance(access_info, access.AccessInfoV3) + self.assertEqual(token_id, access_info.auth_token) + + def test_validate_token_invalid(self): + # When the token is invalid the server typically returns a 404. + token_id = uuid.uuid4().hex + self.stub_url('GET', ['auth', 'tokens'], status_code=404) + self.assertRaises(exceptions.NotFound, + self.client.tokens.validate, token_id) + + def test_validate_token_catalog(self): + # Can validate a token and a catalog is requested by default. + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_DEFAULT] + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + access_info = self.client.tokens.validate(token_id) + + self.assertQueryStringIs() + self.assertTrue(access_info.has_service_catalog()) + + def test_validate_token_nocatalog(self): + # Can validate a token and request no catalog. + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_UNSCOPED] + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + access_info = self.client.tokens.validate(token_id, + include_catalog=False) + + self.assertQueryStringIs('nocatalog') + self.assertFalse(access_info.has_service_catalog()) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/v3/test_trusts.py b/keystoneclient/tests/unit/v3/test_trusts.py new file mode 100644 index 0000000..fbd8fde --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_trusts.py @@ -0,0 +1,112 @@ +# 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 oslo_utils import timeutils + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib import trusts + + +class TrustTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(TrustTests, self).setUp() + self.key = 'trust' + self.collection_key = 'trusts' + self.model = trusts.Trust + self.manager = self.client.trusts + self.path_prefix = 'OS-TRUST' + + def new_ref(self, **kwargs): + kwargs = super(TrustTests, self).new_ref(**kwargs) + kwargs.setdefault('project_id', uuid.uuid4().hex) + return kwargs + + def test_create(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + super(TrustTests, self).test_create(ref=ref) + + def test_create_limited_uses(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + ref['remaining_uses'] = 5 + super(TrustTests, self).test_create(ref=ref) + + def test_create_roles(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + req_ref = ref.copy() + req_ref.pop('id') + + # Note the TrustManager takes a list of role_names, and converts + # internally to the slightly odd list-of-dict API format, so we + # have to pass the expected request data to allow correct stubbing + ref['role_names'] = ['atestrole'] + req_ref['roles'] = [{'name': 'atestrole'}] + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_create_expires(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + ref['expires_at'] = timeutils.parse_isotime( + '2013-03-04T12:00:01.000000Z') + req_ref = ref.copy() + req_ref.pop('id') + + # Note the TrustManager takes a datetime.datetime object for + # expires_at, and converts it internally into an iso format datestamp + req_ref['expires_at'] = '2013-03-04T12:00:01.000000Z' + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_create_imp(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = True + super(TrustTests, self).test_create(ref=ref) + + def test_create_roles_imp(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = True + req_ref = ref.copy() + req_ref.pop('id') + ref['role_names'] = ['atestrole'] + req_ref['roles'] = [{'name': 'atestrole'}] + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_list_filter_trustor(self): + expected_query = {'trustor_user_id': '12345'} + super(TrustTests, self).test_list(expected_query=expected_query, + trustor_user='12345') + + def test_list_filter_trustee(self): + expected_query = {'trustee_user_id': '12345'} + super(TrustTests, self).test_list(expected_query=expected_query, + trustee_user='12345') + + def test_update(self): + # Update not supported for the OS-TRUST API + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) diff --git a/keystoneclient/tests/unit/v3/test_users.py b/keystoneclient/tests/unit/v3/test_users.py new file mode 100644 index 0000000..4645d6e --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_users.py @@ -0,0 +1,279 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import users + + +class UserTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(UserTests, self).setUp() + self.key = 'user' + self.collection_key = 'users' + self.model = users.User + self.manager = self.client.users + + def new_ref(self, **kwargs): + kwargs = super(UserTests, self).new_ref(**kwargs) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('default_project_id', uuid.uuid4().hex) + return kwargs + + def test_add_user_to_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + self.stub_url('PUT', + ['groups', group_id, self.collection_key, ref['id']], + status_code=204) + + self.manager.add_to_group(user=ref['id'], group=group_id) + self.assertRaises(exceptions.ValidationError, + self.manager.remove_from_group, + user=ref['id'], + group=None) + + def test_list_users_in_group(self): + group_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['groups', group_id, self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(group=group_id) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_check_user_in_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['groups', group_id, self.collection_key, ref['id']], + status_code=204) + + self.manager.check_in_group(user=ref['id'], group=group_id) + + self.assertRaises(exceptions.ValidationError, + self.manager.check_in_group, + user=ref['id'], + group=None) + + def test_remove_user_from_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['groups', group_id, self.collection_key, ref['id']], + status_code=204) + + self.manager.remove_from_group(user=ref['id'], group=group_id) + self.assertRaises(exceptions.ValidationError, + self.manager.remove_from_group, + user=ref['id'], + group=None) + + def test_create_doesnt_log_password(self): + password = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_entity('POST', [self.collection_key], + status_code=201, entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + param_ref['password'] = password + params = utils.parameterize(param_ref) + + self.manager.create(**params) + + self.assertNotIn(password, self.logger.output) + + def test_create_with_project(self): + # Can create a user with the deprecated project option rather than + # default_project_id. + ref = self.new_ref() + + self.stub_entity('POST', [self.collection_key], + status_code=201, entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + # Use deprecated project_id rather than new default_project_id. + param_ref['project_id'] = param_ref.pop('default_project_id') + params = utils.parameterize(param_ref) + + returned = self.manager.create(**params) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_create_with_project_and_default_project(self): + # Can create a user with the deprecated project and default_project_id. + # The backend call should only pass the default_project_id. + ref = self.new_ref() + + self.stub_entity('POST', + [self.collection_key], + status_code=201, entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + # Add the deprecated project_id in the call, the value will be ignored. + param_ref['project_id'] = 'project' + params = utils.parameterize(param_ref) + + returned = self.manager.create(**params) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_update_doesnt_log_password(self): + password = uuid.uuid4().hex + ref = self.new_ref() + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + self.stub_entity('PATCH', + [self.collection_key, ref['id']], + status_code=200, entity=ref) + + param_ref['password'] = password + params = utils.parameterize(param_ref) + + self.manager.update(ref['id'], **params) + + self.assertNotIn(password, self.logger.output) + + def test_update_with_project(self): + # Can update a user with the deprecated project option rather than + # default_project_id. + ref = self.new_ref() + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + self.stub_entity('PATCH', + [self.collection_key, ref['id']], + status_code=200, entity=ref) + + # Use deprecated project_id rather than new default_project_id. + param_ref['project_id'] = param_ref.pop('default_project_id') + params = utils.parameterize(param_ref) + + returned = self.manager.update(ref['id'], **params) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_update_with_project_and_default_project(self, ref=None): + ref = self.new_ref() + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + self.stub_entity('PATCH', + [self.collection_key, ref['id']], + status_code=200, entity=ref) + + # Add the deprecated project_id in the call, the value will be ignored. + param_ref['project_id'] = 'project' + params = utils.parameterize(param_ref) + + returned = self.manager.update(ref['id'], **params) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_update_password(self): + old_password = uuid.uuid4().hex + new_password = uuid.uuid4().hex + + self.stub_url('POST', + [self.collection_key, self.TEST_USER, 'password']) + self.client.user_id = self.TEST_USER + self.manager.update_password(old_password, new_password) + + exp_req_body = { + 'user': { + 'password': new_password, 'original_password': old_password + } + } + + self.assertEqual(self.TEST_URL + '/users/test/password', + self.requests.last_request.url) + self.assertRequestBodyIs(json=exp_req_body) + self.assertNotIn(old_password, self.logger.output) + self.assertNotIn(new_password, self.logger.output) + + def test_update_password_with_bad_inputs(self): + old_password = uuid.uuid4().hex + new_password = uuid.uuid4().hex + + # users can't unset their password + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + old_password, None) + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + old_password, '') + + # users can't start with empty passwords + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + None, new_password) + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + '', new_password) + + # this wouldn't result in any change anyway + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + None, None) + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + '', '') + password = uuid.uuid4().hex + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + password, password) diff --git a/keystoneclient/tests/unit/v3/utils.py b/keystoneclient/tests/unit/v3/utils.py new file mode 100644 index 0000000..7320687 --- /dev/null +++ b/keystoneclient/tests/unit/v3/utils.py @@ -0,0 +1,332 @@ +# 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 + +import six +from six.moves.urllib import parse as urlparse + +from keystoneclient.tests.unit import utils +from keystoneclient.v3 import client + + +TestResponse = utils.TestResponse + + +def parameterize(ref): + """Rewrites attributes to match the kwarg naming convention in client. + + >>> parameterize({'project_id': 0}) + {'project': 0} + + """ + params = ref.copy() + for key in ref: + if key[-3:] == '_id': + params.setdefault(key[:-3], params.pop(key)) + return params + + +class UnauthenticatedTestCase(utils.TestCase): + """Class used as base for unauthenticated calls.""" + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + +class TestCase(UnauthenticatedTestCase): + + TEST_ADMIN_IDENTITY_ENDPOINT = "http://127.0.0.1:35357/v3" + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "url": "http://cdn.admin-nets.local:8774/v1.0/", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:8774/v1.0", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "interface": "admin" + }], + "type": "nova_compat" + }, { + "endpoints": [{ + "url": "http://nova/novapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://nova/novapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://nova/novapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "compute" + }, { + "endpoints": [{ + "url": "http://glance/glanceapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://glance/glanceapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://glance/glanceapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "internal" + }, { + "url": TEST_ADMIN_IDENTITY_ENDPOINT, + "region": "RegionOne", + "interface": "admin" + }], + "type": "identity" + }, { + "endpoints": [{ + "url": "http://swift/swiftapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://swift/swiftapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://swift/swiftapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "object-store" + }] + + def setUp(self): + super(TestCase, self).setUp() + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url=self.TEST_URL, + endpoint=self.TEST_URL) + + def stub_auth(self, subject_token=None, **kwargs): + if not subject_token: + subject_token = self.TEST_TOKEN + + try: + response_list = kwargs['response_list'] + except KeyError: + headers = kwargs.setdefault('headers', {}) + headers['X-Subject-Token'] = subject_token + else: + for resp in response_list: + headers = resp.setdefault('headers', {}) + headers['X-Subject-Token'] = subject_token + + self.stub_url('POST', ['auth', 'tokens'], **kwargs) + + +class CrudTests(object): + key = None + collection_key = None + model = None + manager = None + path_prefix = None + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault(uuid.uuid4().hex, uuid.uuid4().hex) + return kwargs + + def encode(self, entity): + if isinstance(entity, dict): + return {self.key: entity} + if isinstance(entity, list): + return {self.collection_key: entity} + raise NotImplementedError('Are you sure you want to encode that?') + + def stub_entity(self, method, parts=None, entity=None, id=None, **kwargs): + if entity: + entity = self.encode(entity) + kwargs['json'] = entity + + if not parts: + parts = [self.collection_key] + + if self.path_prefix: + parts.insert(0, self.path_prefix) + + if id: + if not parts: + parts = [] + + parts.append(id) + + self.stub_url(method, parts=parts, **kwargs) + + def assertEntityRequestBodyIs(self, entity): + self.assertRequestBodyIs(json=self.encode(entity)) + + def test_create(self, ref=None, req_ref=None): + ref = ref or self.new_ref() + manager_ref = ref.copy() + manager_ref.pop('id') + + # req_ref argument allows you to specify a different + # signature for the request when the manager does some + # conversion before doing the request (e.g. converting + # from datetime object to timestamp string) + if req_ref: + req_ref = req_ref.copy() + else: + req_ref = ref.copy() + req_ref.pop('id') + + self.stub_entity('POST', entity=req_ref, status_code=201) + + returned = self.manager.create(**parameterize(manager_ref)) + self.assertIsInstance(returned, self.model) + for attr in req_ref: + self.assertEqual( + getattr(returned, attr), + req_ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + # The entity created here may be used in other test cases + return returned + + def test_get(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id']) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def _get_expected_path(self, expected_path=None): + if not expected_path: + if self.path_prefix: + expected_path = 'v3/%s/%s' % (self.path_prefix, + self.collection_key) + else: + expected_path = 'v3/%s' % self.collection_key + + return expected_path + + def test_list(self, ref_list=None, expected_path=None, + expected_query=None, **filter_kwargs): + ref_list = ref_list or [self.new_ref(), self.new_ref()] + expected_path = self._get_expected_path(expected_path) + + self.requests.get(urlparse.urljoin(self.TEST_URL, expected_path), + json=self.encode(ref_list)) + + returned_list = self.manager.list(**filter_kwargs) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + qs_args = self.requests.last_request.qs + qs_args_expected = expected_query or filter_kwargs + for key, value in six.iteritems(qs_args_expected): + self.assertIn(key, qs_args) + # The querystring value is a list. Note we convert the value to a + # string and lower, as the query string is always a string and the + # filter_kwargs may contain non-string values, for example a + # boolean, causing the comaprison to fail. + self.assertIn(str(value).lower(), qs_args[key]) + + # Also check that no query string args exist which are not expected + for key in qs_args: + self.assertIn(key, qs_args_expected) + + def test_list_params(self): + ref_list = [self.new_ref()] + filter_kwargs = {uuid.uuid4().hex: uuid.uuid4().hex} + expected_path = self._get_expected_path() + + self.requests.get(urlparse.urljoin(self.TEST_URL, expected_path), + json=self.encode(ref_list)) + + self.manager.list(**filter_kwargs) + self.assertQueryStringContains(**filter_kwargs) + + def test_find(self, ref=None): + ref = ref or self.new_ref() + ref_list = [ref] + + self.stub_entity('GET', entity=ref_list) + + returned = self.manager.find(name=getattr(ref, 'name', None)) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + if hasattr(ref, 'name'): + self.assertQueryStringIs('name=%s' % ref['name']) + else: + self.assertQueryStringIs('') + + def test_update(self, ref=None, req_ref=None): + ref = ref or self.new_ref() + + self.stub_entity('PATCH', id=ref['id'], entity=ref) + + # req_ref argument allows you to specify a different + # signature for the request when the manager does some + # conversion before doing the request (e.g. converting + # from datetime object to timestamp string) + if req_ref: + req_ref = req_ref.copy() + else: + req_ref = ref.copy() + req_ref.pop('id') + + returned = self.manager.update(ref['id'], **parameterize(req_ref)) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_delete(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity('DELETE', id=ref['id'], status_code=204) + self.manager.delete(ref['id']) |
