diff --git a/keystonemiddleware/tests/__init__.py b/keystonemiddleware/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystonemiddleware/tests/apiclient/__init__.py b/keystonemiddleware/tests/apiclient/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystonemiddleware/tests/apiclient/test_exceptions.py b/keystonemiddleware/tests/apiclient/test_exceptions.py new file mode 100644 index 00000000..2c6c4b16 --- /dev/null +++ b/keystonemiddleware/tests/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.apiclient import exceptions +from keystoneclient.tests 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/keystonemiddleware/tests/auth/__init__.py b/keystonemiddleware/tests/auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystonemiddleware/tests/auth/test_identity_v2.py b/keystonemiddleware/tests/auth/test_identity_v2.py new file mode 100644 index 00000000..a264edd1 --- /dev/null +++ b/keystonemiddleware/tests/auth/test_identity_v2.py @@ -0,0 +1,257 @@ +# 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 httpretty +from six.moves import urllib + +from keystoneclient.auth.identity import v2 +from keystoneclient import exceptions +from keystoneclient.openstack.common import jsonutils +from keystoneclient import session +from keystoneclient.tests 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(httpretty.POST, ['tokens'], **kwargs) + + @httpretty.activate + 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) + s = session.Session(a) + s.get_token() + + 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) + + @httpretty.activate + 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) + s = session.Session(a) + s.get_token() + + 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) + + @httpretty.activate + 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) + s.get_token() + + 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) + + @httpretty.activate + 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) + s.get_token() + + 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) + + @httpretty.activate + def _do_service_url_test(self, base_url, endpoint_filter): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url(httpretty.GET, ['path'], + base_url=base_url, + body='SUCCESS', status=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) + path = "%s/%s" % (urllib.parse.urlparse(base_url).path, 'path') + self.assertEqual(httpretty.last_request().path, 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) + + @httpretty.activate + 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'}) + + @httpretty.activate + def test_full_url_overrides_endpoint_filter(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url(httpretty.GET, [], + base_url='http://testurl/', + body='SUCCESS', status=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') + + @httpretty.activate + 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) + + @httpretty.activate + def test_invalid_auth_response_type(self): + self.stub_url(httpretty.POST, ['tokens'], body='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) + + @httpretty.activate + 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 = [httpretty.Response(body=jsonutils.dumps(resp_data1), + status=200), + httpretty.Response(body=jsonutils.dumps(resp_data2), + status=200)] + + self.stub_auth(responses=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()) + a.invalidate() + self.assertEqual('token2', s.get_token()) diff --git a/keystonemiddleware/tests/auth/test_identity_v3.py b/keystonemiddleware/tests/auth/test_identity_v3.py new file mode 100644 index 00000000..d44c8e7d --- /dev/null +++ b/keystonemiddleware/tests/auth/test_identity_v3.py @@ -0,0 +1,410 @@ +# 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 httpretty +from six.moves import urllib + +from keystoneclient import access +from keystoneclient.auth.identity import v3 +from keystoneclient import exceptions +from keystoneclient.openstack.common import jsonutils +from keystoneclient import session +from keystoneclient.tests 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() + 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 + }, + } + + def stub_auth(self, subject_token=None, **kwargs): + if not subject_token: + subject_token = self.TEST_TOKEN + + self.stub_url(httpretty.POST, ['auth', 'tokens'], + X_Subject_Token=subject_token, **kwargs) + + @httpretty.activate + 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) + + s.get_token() + + 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) + + @httpretty.activate + 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) + s.get_token() + + 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) + + @httpretty.activate + 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) + s.get_token() + + 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) + + @httpretty.activate + 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) + s.get_token() + + 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) + + @httpretty.activate + 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) + + s.get_token() + + 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) + + @httpretty.activate + 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) + s.get_token() + + 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) + + @httpretty.activate + 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) + s.get_token() + + 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) + + @httpretty.activate + 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) + + s.get_token() + + 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) + + @httpretty.activate + def _do_service_url_test(self, base_url, endpoint_filter): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url(httpretty.GET, ['path'], + base_url=base_url, + body='SUCCESS', status=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) + path = "%s/%s" % (urllib.parse.urlparse(base_url).path, 'path') + self.assertEqual(httpretty.last_request().path, 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) + + @httpretty.activate + 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'}) + + @httpretty.activate + def test_full_url_overrides_endpoint_filter(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url(httpretty.GET, [], + base_url='http://testurl/', + body='SUCCESS', status=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') + + @httpretty.activate + 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) + + @httpretty.activate + def test_invalid_auth_response_type(self): + self.stub_url(httpretty.POST, ['auth', 'tokens'], body='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) + + @httpretty.activate + def test_invalidate_response(self): + body = jsonutils.dumps(self.TEST_RESPONSE_DICT) + auth_responses = [httpretty.Response(body=body, + X_Subject_Token='token1', + status=200), + httpretty.Response(body=body, + X_Subject_Token='token2', + status=200)] + + httpretty.register_uri(httpretty.POST, + '%s/auth/tokens' % self.TEST_URL, + responses=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()) + a.invalidate() + self.assertEqual('token2', s.get_token()) diff --git a/keystonemiddleware/tests/auth/test_token_endpoint.py b/keystonemiddleware/tests/auth/test_token_endpoint.py new file mode 100644 index 00000000..c59e17e1 --- /dev/null +++ b/keystonemiddleware/tests/auth/test_token_endpoint.py @@ -0,0 +1,49 @@ +# 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 httpretty + +from keystoneclient.auth import token_endpoint +from keystoneclient import session +from keystoneclient.tests import utils + + +class TokenEndpointTest(utils.TestCase): + + TEST_TOKEN = 'aToken' + TEST_URL = 'http://server/prefix' + + @httpretty.activate + def test_basic_case(self): + httpretty.register_uri(httpretty.GET, self.TEST_URL, body='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) + + @httpretty.activate + def test_basic_endpoint_case(self): + self.stub_url(httpretty.GET, ['p'], body='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) diff --git a/keystonemiddleware/tests/client_fixtures.py b/keystonemiddleware/tests/client_fixtures.py new file mode 100644 index 00000000..d58deb2b --- /dev/null +++ b/keystonemiddleware/tests/client_fixtures.py @@ -0,0 +1,537 @@ +# 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 +import six +import testresources + +from keystoneclient.common import cms +from keystoneclient.openstack.common import jsonutils +from keystoneclient.openstack.common import timeutils +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()) diff --git a/keystonemiddleware/tests/fakes.py b/keystonemiddleware/tests/fakes.py new file mode 100644 index 00000000..d04ad8c5 --- /dev/null +++ b/keystonemiddleware/tests/fakes.py @@ -0,0 +1,118 @@ +# 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. + +""" +A fake server that "responds" to API methods with pre-canned responses. + +All of these responses come from the spec, so if for some reason the spec's +wrong the tests might raise AssertionError. I've indicated in comments the +places where actual behavior differs from the spec. +""" + +from keystoneclient import access + + +def assert_has_keys(dict, required=[], optional=[]): + keys = dict.keys() + for k in required: + try: + assert k in keys + except AssertionError: + extra_keys = set(keys).difference(set(required + optional)) + raise AssertionError("found unexpected keys: %s" % + list(extra_keys)) + + +class FakeClient(object): + + def assert_called(self, method, url, body=None, pos=-1): + """Assert than an API method was just called.""" + expected = (method, url) + called = self.callstack[pos][0:2] + + assert self.callstack, ("Expected %s %s but no calls were made." % + expected) + assert expected == called, ("Expected %s %s; got %s %s" % + (expected + called)) + + if body is not None: + assert self.callstack[pos][2] == body + + def assert_called_anytime(self, method, url, body=None): + """Assert than an API method was called anytime in the test.""" + expected = (method, url) + + assert self.callstack, ("Expected %s %s but no calls were made." % + expected) + + found = False + for entry in self.callstack: + if expected == entry[0:2]: + found = True + break + + assert found, ('Expected %s; got %s' % + (expected, self.callstack)) + if body is not None: + if entry[2] != body: + raise AssertionError('%s != %s' % (entry[2], body)) + self.callstack = [] + + def clear_callstack(self): + self.callstack = [] + + def authenticate(self, cl_obj): + cl_obj.user_id = '1' + cl_obj.auth_user_id = '1' + cl_obj.project_id = '1' + cl_obj.auth_tenant_id = '1' + cl_obj.auth_ref = access.AccessInfo.factory(None, { + "access": { + "token": { + "expires": "2012-02-05T00:00:00", + "id": "887665443383838", + "tenant": { + "id": "1", + "name": "customer-x" + } + }, + "serviceCatalog": [{ + "endpoints": [{ + "adminURL": "http://swift.admin-nets.local:8080/", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8080/v1/AUTH_1", + "publicURL": + "http://swift.publicinternets.com/v1/AUTH_1" + }], + "type": "object-store", + "name": "swift" + }, { + "endpoints": [{ + "adminURL": "http://cdn.admin-nets.local/v1.1/1", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:7777/v1.1/1", + "publicURL": "http://cdn.publicinternets.com/v1.1/1" + }], + "type": "object-store", + "name": "cdn" + }], + "user": { + "id": "1", + "roles": [{ + "tenantId": "1", + "id": "3", + "name": "Member" + }], + "name": "joeuser" + } + } + }) diff --git a/keystonemiddleware/tests/generic/__init__.py b/keystonemiddleware/tests/generic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystonemiddleware/tests/generic/test_client.py b/keystonemiddleware/tests/generic/test_client.py new file mode 100644 index 00000000..1ea67cbb --- /dev/null +++ b/keystonemiddleware/tests/generic/test_client.py @@ -0,0 +1,67 @@ +# 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 httpretty + +from keystoneclient.generic import client +from keystoneclient.openstack.common import jsonutils +from keystoneclient.tests 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]) + + +@httpretty.activate +class ClientDiscoveryTests(utils.TestCase): + + def test_discover_extensions_v2(self): + httpretty.register_uri(httpretty.GET, "%s/extensions" % V2_URL, + body=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/keystonemiddleware/tests/generic/test_shell.py b/keystonemiddleware/tests/generic/test_shell.py new file mode 100644 index 00000000..e30b056e --- /dev/null +++ b/keystonemiddleware/tests/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 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 + + :return: 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/keystonemiddleware/tests/test_auth_token_middleware.py b/keystonemiddleware/tests/test_auth_token_middleware.py new file mode 100644 index 00000000..cb045d00 --- /dev/null +++ b/keystonemiddleware/tests/test_auth_token_middleware.py @@ -0,0 +1,1999 @@ +# 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 httpretty +import iso8601 +import mock +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 jsonutils +from keystoneclient.openstack.common import memorycache +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests import client_fixtures +from keystoneclient.tests 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_v3 = jsonutils.dumps({ + "versions": { + "values": [ + { + "id": "v3.0", + "status": "stable", + "updated": "2013-03-06T00:00:00Z", + "links": [{'href': '%s/v3' % BASE_URI, 'rel': 'self'}] + }, + { + "id": "v2.0", + "status": "stable", + "updated": "2011-11-19T00:00:00Z", + "links": [{'href': '%s/v2.0' % BASE_URI, 'rel': 'self'}] + } + ] + } +}) + +VERSION_LIST_v2 = jsonutils.dumps({ + "versions": { + "values": [ + { + "id": "v2.0", + "status": "stable", + "updated": "2011-11-19T00:00:00Z", + "links": [] + } + ] + } +}) + +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 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 + + 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: + self.assertEqual(path, httpretty.last_request().path) + else: + self.assertIsInstance(httpretty.last_request(), + httpretty.core.HTTPrettyRequestEmpty) + + +class MultiStepAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + @httpretty.activate + 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. + httpretty.register_uri(httpretty.POST, "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + responses = [httpretty.Response(body='', status=401), + httpretty.Response( + body=self.examples.SIGNED_REVOCATION_LIST)] + + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/revoked" % BASE_URI, + responses=responses) + + 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(httpretty.httpretty.latest_requests), 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) + + httpretty.reset() + httpretty.enable() + self.addCleanup(httpretty.disable) + + httpretty.register_uri(httpretty.GET, + "%s/" % BASE_URI, + body=VERSION_LIST_v2, + status=300) + + httpretty.register_uri(httpretty.POST, + "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + self.token_id = self.examples.VALID_DIABLO_TOKEN + token_response = self.examples.JSON_TOKEN_RESPONSES[self.token_id] + + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/%s" % (BASE_URI, self.token_id), + body=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): + httpretty.disable() + 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): + httpretty.disable() + 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): + httpretty.disable() + 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)) + + +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_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_pki_token(self): + # 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['signed_token_scoped'] + + # 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]) + + # First, 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 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 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): + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/revoked" % BASE_URI, + body="{}", + status=200) + + 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 + httpretty.register_uri(httpretty.GET, invalid_uri, body="", status=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): + # NOTE(jamielennox): it appears that httpretty can mess with the + # memcache socket. Just disable it as it's not required here anyway. + httpretty.disable() + 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): + httpretty.disable() + 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 + httpretty.register_uri(httpretty.GET, invalid_uri, body="", status=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={}): + httpretty.disable() + 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) + timeutils_utcnow = 'keystoneclient.openstack.common.timeutils.utcnow' + now = datetime.datetime.utcnow() + with mock.patch(timeutils_utcnow) as mock_utcnow: + mock_utcnow.return_value = now + self.middleware(req.environ, self.start_fake_response) + self.assertIsNotNone(self._get_cached_token(token)) + expired = now + datetime.timedelta(seconds=token_cache_time) + with mock.patch(timeutils_utcnow) as mock_utcnow: + mock_utcnow.return_value = expired + 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) + + httpretty.reset() + httpretty.enable() + self.addCleanup(httpretty.disable) + + # 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() + + httpretty.register_uri(httpretty.GET, + "%s%s" % (BASE_URI, self.ca_path), + status=404) + httpretty.register_uri(httpretty.GET, + "%s%s" % (BASE_URI, self.signing_path), + status=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' + httpretty.register_uri(httpretty.GET, + "%s%s" % (BASE_URI, self.signing_path), + body=data) + self.middleware.fetch_signing_cert() + + with open(self.middleware.signing_cert_file_name, 'r') as f: + self.assertEqual(f.read(), data) + + self.assertEqual("/testadmin%s" % self.signing_path, + httpretty.last_request().path) + + def test_fetch_signing_ca(self): + data = 'FAKE CA' + httpretty.register_uri(httpretty.GET, + "%s%s" % (BASE_URI, self.ca_path), + body=data) + self.middleware.fetch_ca_cert() + + with open(self.middleware.signing_ca_file_name, 'r') as f: + self.assertEqual(f.read(), data) + + self.assertEqual("/testadmin%s" % self.ca_path, + httpretty.last_request().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/' + + httpretty.register_uri(httpretty.GET, + "%s/newadmin%s" % (BASE_HOST, self.ca_path), + body='FAKECA') + httpretty.register_uri(httpretty.GET, + "%s/newadmin%s" % + (BASE_HOST, self.signing_path), body='FAKECERT') + + self.set_middleware(conf=self.conf) + + self.middleware.fetch_ca_cert() + + self.assertEqual('/newadmin%s' % self.ca_path, + httpretty.last_request().path) + + self.middleware.fetch_signing_cert() + + self.assertEqual('/newadmin%s' % self.signing_path, + httpretty.last_request().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'] = '' + + httpretty.register_uri(httpretty.GET, + "%s%s" % (BASE_HOST, self.ca_path), + body='FAKECA') + httpretty.register_uri(httpretty.GET, + "%s%s" % (BASE_HOST, self.signing_path), + body='FAKECERT') + + self.set_middleware(conf=self.conf) + + self.middleware.fetch_ca_cert() + + self.assertEqual(self.ca_path, + httpretty.last_request().path) + + self.middleware.fetch_signing_cert() + + self.assertEqual(self.signing_path, + httpretty.last_request().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, + } + + httpretty.reset() + httpretty.enable() + self.addCleanup(httpretty.disable) + + httpretty.register_uri(httpretty.GET, + "%s/" % BASE_URI, + body=VERSION_LIST_v2, + status=300) + + httpretty.register_uri(httpretty.POST, + "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/revoked" % BASE_URI, + body=self.examples.SIGNED_REVOCATION_LIST, + status=200) + + 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,): + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/%s" % (BASE_URI, token), + body= + self.examples.JSON_TOKEN_RESPONSES[token]) + + httpretty.register_uri(httpretty.GET, + '%s/v2.0/tokens/%s' % (BASE_URI, ERROR_TOKEN), + body=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)] + + @httpretty.activate + 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' + } + + httpretty.register_uri(httpretty.GET, + "%s/" % BASE_URI, + body=VERSION_LIST_v3, + status=300) + + httpretty.register_uri(httpretty.POST, + "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + token = self.examples.UUID_TOKEN_DEFAULT + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/%s" % (BASE_URI, token), + body= + self.examples.JSON_TOKEN_RESPONSES[token]) + + 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.assertEqual("/testadmin/v2.0/tokens/%s" % + self.examples.UUID_TOKEN_DEFAULT, + httpretty.last_request().path) + + +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, + } + + httpretty.reset() + httpretty.enable() + self.addCleanup(httpretty.disable) + + httpretty.register_uri(httpretty.GET, + "%s" % BASE_URI, + body=VERSION_LIST_v3, + status=300) + + # TODO(jamielennox): auth_token middleware uses a v2 admin token + # regardless of the auth_version that is set. + httpretty.register_uri(httpretty.POST, + "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + # TODO(jamielennox): there is no v3 revocation url yet, it uses v2 + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/revoked" % BASE_URI, + body=self.examples.SIGNED_REVOCATION_LIST, + status=200) + + httpretty.register_uri(httpretty.GET, + "%s/v3/auth/tokens" % BASE_URI, + body=self.token_response) + + self.set_middleware() + + def token_response(self, request, uri, headers): + auth_id = request.headers.get('X-Auth-Token') + token_id = request.headers.get('X-Subject-Token') + self.assertEqual(auth_id, FAKE_ADMIN_TOKEN_ID) + headers.pop('status') + + status = 200 + response = "" + + if token_id == ERROR_TOKEN: + raise auth_token.NetworkError("Network connection error.") + + try: + response = self.examples.JSON_TOKEN_RESPONSES[token_id] + except KeyError: + status = 404 + + return status, headers, 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) + + @mock.patch('keystoneclient.openstack.common.timeutils.utcnow') + def test_v2_token_with_timezone_offset_not_expired(self, mock_utcnow): + current_time = timeutils.parse_isotime('2000-01-01T00:01:10.000123Z') + current_time = timeutils.normalize_time(current_time) + mock_utcnow.return_value = current_time + 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) + + @mock.patch('keystoneclient.openstack.common.timeutils.utcnow') + def test_v2_token_with_timezone_offset_expired(self, mock_utcnow): + current_time = timeutils.parse_isotime('2000-01-01T00:01:10.000123Z') + current_time = timeutils.normalize_time(current_time) + mock_utcnow.return_value = current_time + 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) + + @mock.patch('keystoneclient.openstack.common.timeutils.utcnow') + def test_v3_token_with_timezone_offset_not_expired(self, mock_utcnow): + current_time = timeutils.parse_isotime('2000-01-01T00:01:10.000123Z') + current_time = timeutils.normalize_time(current_time) + mock_utcnow.return_value = current_time + 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) + + @mock.patch('keystoneclient.openstack.common.timeutils.utcnow') + def test_v3_token_with_timezone_offset_expired(self, mock_utcnow): + current_time = timeutils.parse_isotime('2000-01-01T00:01:10.000123Z') + current_time = timeutils.normalize_time(current_time) + mock_utcnow.return_value = current_time + 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/keystonemiddleware/tests/test_base.py b/keystonemiddleware/tests/test_base.py new file mode 100644 index 00000000..023d8074 --- /dev/null +++ b/keystonemiddleware/tests/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 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), "") + + 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.get = self.mox.CreateMockAnything() + self.client.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/keystonemiddleware/tests/test_cms.py b/keystonemiddleware/tests/test_cms.py new file mode 100644 index 00000000..43eba2b9 --- /dev/null +++ b/keystonemiddleware/tests/test_cms.py @@ -0,0 +1,149 @@ +# 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 subprocess + +import mock +import testresources +from testtools import matchers + +from keystoneclient.common import cms +from keystoneclient import exceptions +from keystoneclient.tests import client_fixtures +from keystoneclient.tests import utils + + +class CMSTest(utils.TestCase, testresources.ResourcedTestCase): + + """Unit tests for the keystoneclient.common.cms module.""" + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + 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): + import errno + + 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 subprocess.CalledProcessError as e: + self.assertIn('/no/such/file', e.output) + self.assertIn('Hit OSError ', e.output) + else: + self.fail('Expected subprocess.CalledProcessError') + + 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/keystonemiddleware/tests/test_discovery.py b/keystonemiddleware/tests/test_discovery.py new file mode 100644 index 00000000..7d8b4711 --- /dev/null +++ b/keystonemiddleware/tests/test_discovery.py @@ -0,0 +1,790 @@ +# 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 httpretty +import six +from testtools import matchers + +from keystoneclient import _discover +from keystoneclient import client +from keystoneclient import discover +from keystoneclient import exceptions +from keystoneclient.openstack.common import jsonutils +from keystoneclient.tests 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_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/' + 'openstack-identity-service/2.0/content/', + 'rel': 'describedby', + 'type': 'text/html'} +V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident' + 'ity-service/2.0/identity-dev-guide-2.0.pdf', + 'rel': 'describedby', + 'type': 'application/pdf'} + +V2_VERSION = {'id': 'v2.0', + 'links': [{'href': V2_URL, 'rel': 'self'}, + V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF], + 'status': 'stable', + 'updated': 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_MEDIA_TYPES = [{'base': 'application/json', + 'type': 'application/vnd.openstack.identity-v3+json'}, + {'base': 'application/xml', + 'type': 'application/vnd.openstack.identity-v3+xml'}] + +V3_VERSION = {'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': 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) + + +@httpretty.activate +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, ex in six.iteritems(examples): + url = "%s%s" % (BASE_URL, path) + + httpretty.register_uri(httpretty.GET, url, status=300, body=ex) + 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): + httpretty.register_uri(httpretty.GET, V3_URL, status=200, + body=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): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=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): + body = jsonutils.dumps(CINDER_EXAMPLES) + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, body=body) + + 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): + body = jsonutils.dumps(GLANCE_EXAMPLES) + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body) + + 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") + + +@httpretty.activate +class ClientDiscoveryTests(utils.TestCase): + + def assertCreatesV3(self, **kwargs): + httpretty.register_uri(httpretty.POST, "%s/auth/tokens" % V3_URL, + body=V3_AUTH_RESPONSE, 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): + httpretty.register_uri(httpretty.POST, "%s/tokens" % V2_URL, + body=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): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=V3_VERSION_LIST) + + self.assertCreatesV3(auth_url=BASE_URL) + + def test_discover_v2(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=V2_VERSION_LIST) + httpretty.register_uri(httpretty.POST, "%s/tokens" % V2_URL, + body=V2_AUTH_RESPONSE) + + self.assertCreatesV2(auth_url=BASE_URL) + + def test_discover_endpoint_v2(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=V2_VERSION_LIST) + self.assertCreatesV2(endpoint=BASE_URL, token='fake-token') + + def test_discover_endpoint_v3(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=V3_VERSION_LIST) + self.assertCreatesV3(endpoint=BASE_URL, token='fake-token') + + def test_discover_invalid_major_version(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=V3_VERSION_LIST) + + self.assertVersionNotAvailable(auth_url=BASE_URL, version=5) + + def test_discover_200_response_fails(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body='ok') + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_discover_minor_greater_than_available_fails(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=V3_VERSION_LIST) + + self.assertVersionNotAvailable(endpoint=BASE_URL, version=3.4) + + def test_discover_individual_version_v2(self): + httpretty.register_uri(httpretty.GET, V2_URL, status=200, + body=V2_VERSION_ENTRY) + + self.assertCreatesV2(auth_url=V2_URL) + + def test_discover_individual_version_v3(self): + httpretty.register_uri(httpretty.GET, V3_URL, status=200, + body=V3_VERSION_ENTRY) + + self.assertCreatesV3(auth_url=V3_URL) + + def test_discover_individual_endpoint_v2(self): + httpretty.register_uri(httpretty.GET, V2_URL, status=200, + body=V2_VERSION_ENTRY) + self.assertCreatesV2(endpoint=V2_URL, token='fake-token') + + def test_discover_individual_endpoint_v3(self): + httpretty.register_uri(httpretty.GET, V3_URL, status=200, + body=V3_VERSION_ENTRY) + self.assertCreatesV3(endpoint=V3_URL, token='fake-token') + + def test_discover_fail_to_create_bad_individual_version(self): + httpretty.register_uri(httpretty.GET, V2_URL, status=200, + body=V2_VERSION_ENTRY) + httpretty.register_uri(httpretty.GET, V3_URL, status=200, + body=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): + v3_unstable_version = V3_VERSION.copy() + v3_unstable_version['status'] = 'beta' + version_list = _create_version_list([v3_unstable_version, V2_VERSION]) + + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=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): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=V3_VERSION_LIST) + + ip = '192.168.1.1' + self.assertCreatesV3(auth_url=BASE_URL, original_ip=ip) + + self.assertThat(httpretty.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): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=jsonutils.dumps({'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}] + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=_create_version_list(resp)) + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_ignore_entry_without_links(self): + v3 = V3_VERSION.copy() + v3['links'] = [] + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=_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'] + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=_create_version_list([v3, V2_VERSION])) + self.assertCreatesV2(auth_url=BASE_URL) + + def test_greater_version_than_required(self): + resp = [{'id': 'v3.6', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED}] + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, + body=_create_version_list(resp)) + self.assertCreatesV3(auth_url=BASE_URL, version=(3, 4)) + + def test_lesser_version_than_required(self): + resp = [{'id': 'v3.4', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED}] + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, + body=_create_version_list(resp)) + self.assertVersionNotAvailable(auth_url=BASE_URL, version=(3, 6)) + + def test_bad_response(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body="Ugly Duckling") + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_pass_client_arguments(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=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): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=V3_VERSION_LIST) + + httpretty.register_uri(httpretty.POST, "%s/auth/tokens" % V3_URL, + body=V3_AUTH_RESPONSE, 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): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=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} + body = _create_version_list([V4_VERSION, V3_VERSION, V2_VERSION]) + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, body=body) + + disc = discover.Discover(auth_url=BASE_URL) + self.assertRaises(exceptions.DiscoveryFailure, + disc.create_client, version=4) + + +@httpretty.activate +class DiscoverQueryTests(utils.TestCase): + + def test_available_keystone_data(self): + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, + body=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): + body = jsonutils.dumps(CINDER_EXAMPLES) + httpretty.register_uri(httpretty.GET, BASE_URL, status=300, body=body) + + 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): + body = jsonutils.dumps(GLANCE_EXAMPLES) + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body) + + 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}] + body = jsonutils.dumps({'versions': version_list}) + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body) + + 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}] + body = jsonutils.dumps({'versions': version_list}) + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body) + + 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 = [{'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': status, + 'updated': UPDATED}] + body = jsonutils.dumps({'versions': version_list}) + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body) + + 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'}], + }] + + body = jsonutils.dumps({'versions': version_list}) + httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body) + + 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 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/keystonemiddleware/tests/test_ec2utils.py b/keystonemiddleware/tests/test_ec2utils.py new file mode 100644 index 00000000..ff4aee35 --- /dev/null +++ b/keystonemiddleware/tests/test_ec2utils.py @@ -0,0 +1,252 @@ +# 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 = {} + 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/keystonemiddleware/tests/test_http.py b/keystonemiddleware/tests/test_http.py new file mode 100644 index 00000000..e089b7c9 --- /dev/null +++ b/keystonemiddleware/tests/test_http.py @@ -0,0 +1,216 @@ +# 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 httpretty +import six +from testtools import matchers + +from keystoneclient import exceptions +from keystoneclient import httpclient +from keystoneclient import session +from keystoneclient.tests 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') + + @httpretty.activate + def test_get(self): + cl = get_authed_client() + + self.stub_url(httpretty.GET, body=RESPONSE_BODY) + + resp, body = cl.get("/hi") + self.assertEqual(httpretty.last_request().method, 'GET') + self.assertEqual(httpretty.last_request().path, '/hi') + + self.assertRequestHeaderEqual('X-Auth-Token', 'token') + self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT) + + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + @httpretty.activate + def test_get_error_with_plaintext_resp(self): + cl = get_authed_client() + self.stub_url(httpretty.GET, status=400, + body='Some evil plaintext string') + + self.assertRaises(exceptions.BadRequest, cl.get, '/hi') + + @httpretty.activate + 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(httpretty.GET, status=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.') + + @httpretty.activate + def test_post(self): + cl = get_authed_client() + + self.stub_url(httpretty.POST) + cl.post("/hi", body=[1, 2, 3]) + + self.assertEqual(httpretty.last_request().method, 'POST') + self.assertEqual(httpretty.last_request().body, b'[1, 2, 3]') + + self.assertRequestHeaderEqual('X-Auth-Token', 'token') + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT) + + @httpretty.activate + 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(httpretty.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=200, + url=None, **kwargs): + if not url: + url = self.url + + httpretty.register_uri(method, url, body=response, status=status) + + return httpclient.request(url, method, **kwargs) + + @httpretty.activate + def test_basic_params(self): + method = 'GET' + response = 'Test Response' + status = 200 + + self.request(method=method, status=status, response=response) + + self.assertEqual(httpretty.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)) + + @httpretty.activate + 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)) + + @httpretty.activate + 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/keystonemiddleware/tests/test_https.py b/keystonemiddleware/tests/test_https.py new file mode 100644 index 00000000..f9618be9 --- /dev/null +++ b/keystonemiddleware/tests/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 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/keystonemiddleware/tests/test_keyring.py b/keystonemiddleware/tests/test_keyring.py new file mode 100644 index 00000000..24f4bb5f --- /dev/null +++ b/keystonemiddleware/tests/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 keystoneclient import access +from keystoneclient import httpclient +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests import utils +from keystoneclient.tests.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 + with mock.patch.object(cl, 'get_raw_token_from_identity_service') \ + 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 + with mock.patch.object(cl, 'get_raw_token_from_identity_service') \ + 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 + with mock.patch.object(cl, 'get_raw_token_from_identity_service') \ + 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/keystonemiddleware/tests/test_memcache_crypt.py b/keystonemiddleware/tests/test_memcache_crypt.py new file mode 100644 index 00000000..be07b24e --- /dev/null +++ b/keystonemiddleware/tests/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/keystonemiddleware/tests/test_s3_token_middleware.py b/keystonemiddleware/tests/test_s3_token_middleware.py new file mode 100644 index 00000000..c3272cc3 --- /dev/null +++ b/keystonemiddleware/tests/test_s3_token_middleware.py @@ -0,0 +1,236 @@ +# 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 httpretty +import mock +import requests +import six +import testtools +import webob + +from keystoneclient.middleware import s3_token +from keystoneclient.openstack.common import jsonutils +from keystoneclient.tests 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, + } + + httpretty.reset() + httpretty.enable() + self.addCleanup(httpretty.disable) + + 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) + + httpretty.register_uri(httpretty.POST, self.TEST_URL, + status=201, body=jsonutils.dumps(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): + 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"}} + httpretty.register_uri(httpretty.POST, self.TEST_URL, + status=403, body=jsonutils.dumps(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): + httpretty.register_uri(httpretty.POST, self.TEST_URL, + status=201, body="") + + 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/keystonemiddleware/tests/test_session.py b/keystonemiddleware/tests/test_session.py new file mode 100644 index 00000000..f1e616e1 --- /dev/null +++ b/keystonemiddleware/tests/test_session.py @@ -0,0 +1,508 @@ +# 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 httpretty +import mock +import requests +import six + +from keystoneclient.auth import base +from keystoneclient import exceptions +from keystoneclient import session as client_session +from keystoneclient.tests import utils + + +class SessionTests(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/' + + @httpretty.activate + def test_get(self): + session = client_session.Session() + self.stub_url(httpretty.GET, body='response') + resp = session.get(self.TEST_URL) + + self.assertEqual(httpretty.GET, httpretty.last_request().method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + + @httpretty.activate + def test_post(self): + session = client_session.Session() + self.stub_url(httpretty.POST, body='response') + resp = session.post(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual(httpretty.POST, httpretty.last_request().method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + self.assertRequestBodyIs(json={'hello': 'world'}) + + @httpretty.activate + def test_head(self): + session = client_session.Session() + self.stub_url(httpretty.HEAD) + resp = session.head(self.TEST_URL) + + self.assertEqual(httpretty.HEAD, httpretty.last_request().method) + self.assertTrue(resp.ok) + self.assertRequestBodyIs('') + + @httpretty.activate + def test_put(self): + session = client_session.Session() + self.stub_url(httpretty.PUT, body='response') + resp = session.put(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual(httpretty.PUT, httpretty.last_request().method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + self.assertRequestBodyIs(json={'hello': 'world'}) + + @httpretty.activate + def test_delete(self): + session = client_session.Session() + self.stub_url(httpretty.DELETE, body='response') + resp = session.delete(self.TEST_URL) + + self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + self.assertTrue(resp.ok) + self.assertEqual(resp.text, 'response') + + @httpretty.activate + def test_patch(self): + session = client_session.Session() + self.stub_url(httpretty.PATCH, body='response') + resp = session.patch(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual(httpretty.PATCH, httpretty.last_request().method) + self.assertTrue(resp.ok) + self.assertEqual(resp.text, 'response') + self.assertRequestBodyIs(json={'hello': 'world'}) + + @httpretty.activate + def test_user_agent(self): + session = client_session.Session(user_agent='test-agent') + self.stub_url(httpretty.GET, body='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') + + @httpretty.activate + 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) + + @httpretty.activate + def test_not_found(self): + session = client_session.Session() + self.stub_url(httpretty.GET, status=404) + self.assertRaises(exceptions.NotFound, session.get, self.TEST_URL) + + @httpretty.activate + def test_server_error(self): + session = client_session.Session() + self.stub_url(httpretty.GET, status=500) + self.assertRaises(exceptions.InternalServerError, + session.get, self.TEST_URL) + + @httpretty.activate + def test_session_debug_output(self): + session = client_session.Session(verify=False) + headers = {'HEADERA': 'HEADERVALB'} + body = 'BODYRESPONSE' + data = 'BODYDATA' + self.stub_url(httpretty.POST, body=body) + session.post(self.TEST_URL, headers=headers, data=data) + + 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) + + +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=httpretty.GET, status=305, + redirect_kwargs={}, final_kwargs={}): + redirect_kwargs.setdefault('body', self.DEFAULT_REDIRECT_BODY) + + for s, d in zip(self.REDIRECT_CHAIN, self.REDIRECT_CHAIN[1:]): + httpretty.register_uri(method, s, status=status, location=d, + **redirect_kwargs) + + final_kwargs.setdefault('status', 200) + final_kwargs.setdefault('body', self.DEFAULT_RESP_BODY) + httpretty.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) + + @httpretty.activate + def test_basic_get(self): + session = client_session.Session() + self.setup_redirects() + resp = session.get(self.REDIRECT_CHAIN[-2]) + self.assertResponse(resp) + + @httpretty.activate + def test_basic_post_keeps_correct_method(self): + session = client_session.Session() + self.setup_redirects(method=httpretty.POST, status=301) + resp = session.post(self.REDIRECT_CHAIN[-2]) + self.assertResponse(resp) + + @httpretty.activate + 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)) + + @httpretty.activate + 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]) + + @httpretty.activate + 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) + + @httpretty.activate + def test_history_matches_requests(self): + self.setup_redirects(status=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' + + 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 + + +class CalledAuthPlugin(base.BaseAuthPlugin): + + ENDPOINT = 'http://fakeendpoint/' + + def __init__(self, invalidate=True): + self.get_token_called = False + self.get_endpoint_called = False + 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 + 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=httpretty.GET, **kwargs): + base_url = AuthPlugin.SERVICE_URLS[service_type][interface] + uri = "%s/%s" % (base_url.rstrip('/'), path.lstrip('/')) + + httpretty.register_uri(method, uri, **kwargs) + + @httpretty.activate + def test_auth_plugin_default_with_plugin(self): + self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON) + + # if there is an auth_plugin then it should default to authenticated + auth = AuthPlugin() + sess = client_session.Session(auth=auth) + resp = sess.get(self.TEST_URL) + self.assertDictEqual(resp.json(), self.TEST_JSON) + + self.assertRequestHeaderEqual('X-Auth-Token', AuthPlugin.TEST_TOKEN) + + @httpretty.activate + def test_auth_plugin_disable(self): + self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON) + + auth = AuthPlugin() + sess = client_session.Session(auth=auth) + resp = sess.get(self.TEST_URL, authenticated=False) + self.assertDictEqual(resp.json(), self.TEST_JSON) + + self.assertRequestHeaderEqual('X-Auth-Token', None) + + @httpretty.activate + 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=status, + body=body) + + sess = client_session.Session(auth=AuthPlugin()) + resp = sess.get(path, + endpoint_filter={'service_type': service_type, + 'interface': interface}) + + self.assertEqual(httpretty.last_request().path, '/v1.0/instances') + 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'}) + + @httpretty.activate + 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.stub_url(httpretty.GET, status=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) + + @httpretty.activate + def test_passed_auth_plugin(self): + passed = CalledAuthPlugin() + sess = client_session.Session() + + httpretty.register_uri(httpretty.GET, + CalledAuthPlugin.ENDPOINT + 'path', + status=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) + + @httpretty.activate + def test_passed_auth_plugin_overrides(self): + fixed = CalledAuthPlugin() + passed = CalledAuthPlugin() + + sess = client_session.Session(fixed) + + httpretty.register_uri(httpretty.GET, + CalledAuthPlugin.ENDPOINT + 'path', + status=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) + + @httpretty.activate + def test_reauth_called(self): + auth = CalledAuthPlugin(invalidate=True) + sess = client_session.Session(auth=auth) + + responses = [httpretty.Response(body='Failed', status=401), + httpretty.Response(body='Hello', status=200)] + httpretty.register_uri(httpretty.GET, self.TEST_URL, + responses=responses) + + # 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) + + @httpretty.activate + def test_reauth_not_called(self): + auth = CalledAuthPlugin(invalidate=True) + sess = client_session.Session(auth=auth) + + responses = [httpretty.Response(body='Failed', status=401), + httpretty.Response(body='Hello', status=200)] + httpretty.register_uri(httpretty.GET, self.TEST_URL, + responses=responses) + + self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL, + authenticated=True, allow_reauth=False) + self.assertFalse(auth.invalidate_called) diff --git a/keystonemiddleware/tests/test_shell.py b/keystonemiddleware/tests/test_shell.py new file mode 100644 index 00000000..da9875f6 --- /dev/null +++ b/keystonemiddleware/tests/test_shell.py @@ -0,0 +1,517 @@ +# 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 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/' + + +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])) + + # Make a fake shell object, a helping wrapper to call it, and a quick + # way of asserting that certain API calls were made. + global shell, _shell, assert_called, assert_called_anytime + _shell = openstack_shell.OpenStackIdentityShell() + shell = lambda cmd: _shell.main(cmd.split()) + + 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_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=secrete --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', 'secrete', '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=secrete --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', 'secrete', '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=secrete --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', 'secrete', '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=secrete --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', 'secrete', '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=secrete --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', 'secrete', '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)])) diff --git a/keystonemiddleware/tests/test_utils.py b/keystonemiddleware/tests/test_utils.py new file mode 100644 index 00000000..01131e6f --- /dev/null +++ b/keystonemiddleware/tests/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 import client_fixtures +from keystoneclient.tests 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/keystonemiddleware/tests/utils.py b/keystonemiddleware/tests/utils.py new file mode 100644 index 00000000..9285b385 --- /dev/null +++ b/keystonemiddleware/tests/utils.py @@ -0,0 +1,199 @@ +# 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 fixtures +import httpretty +import mock +from mox3 import mox +import requests +import six +from six.moves.urllib import parse as urlparse +import testtools +import uuid + +from keystoneclient.openstack.common import jsonutils + + +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() + + 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['body'] = jsonutils.dumps(json) + kwargs['content_type'] = 'application/json' + + if parts: + url = '/'.join([p.strip('/') for p in [base_url] + parts]) + else: + url = base_url + + # For urls containing queries + url = url.replace("/?", "?") + httpretty.register_uri(method, url, **kwargs) + + def assertRequestBodyIs(self, body=None, json=None): + last_request_body = httpretty.last_request().body + if six.PY3: + last_request_body = last_request_body.decode('utf-8') + + 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) + self.assertEqual(expected, httpretty.last_request().querystring) + + def assertQueryStringContains(self, **kwargs): + qs = httpretty.last_request().querystring + + 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 and httpretty must have been + activated for the request. + """ + headers = httpretty.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/keystonemiddleware/tests/v2_0/__init__.py b/keystonemiddleware/tests/v2_0/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystonemiddleware/tests/v2_0/client_fixtures.py b/keystonemiddleware/tests/v2_0/client_fixtures.py new file mode 100644 index 00000000..178b1487 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/client_fixtures.py @@ -0,0 +1,125 @@ +# 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='edc12489faa74ee0aca0b8a0b4d74a74', + 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/keystonemiddleware/tests/v2_0/fakes.py b/keystonemiddleware/tests/v2_0/fakes.py new file mode 100644 index 00000000..214ea714 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/fakes.py @@ -0,0 +1,495 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 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 six.moves.urllib import parse as urlparse + +from keystoneclient.tests import fakes +from keystoneclient.tests.v2_0 import utils + + +class FakeHTTPClient(fakes.FakeClient): + def __init__(self, **kwargs): + self.username = 'username' + self.password = 'password' + self.auth_url = 'auth_url' + self.callstack = [] + + def _cs_request(self, url, method, **kwargs): + # Check that certain things are called correctly + if method in ['GET', 'DELETE']: + assert 'body' not in kwargs + elif method == 'PUT': + kwargs.setdefault('body', None) + + # Call the method + args = urlparse.parse_qsl(urlparse.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') + munged_url = munged_url.replace('-', '_') + + callback = "%s_%s" % (method.lower(), munged_url) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + # Note the call + self.callstack.append((method, url, kwargs.get('body'))) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + # Note the call + self.callstack.append((method, url, kwargs.get('body'))) + + status, body = getattr(self, callback)(**kwargs) + r = utils.TestResponse({ + "status_code": status, + "text": body}) + return r, body + + # + # List all extensions + # + def post_tokens(self, **kw): + body = [ + {"access": + {"token": + {"expires": "2012-02-05T00:00:00", + "id": "887665443383838", + "tenant": + {"id": "1", + "name": "customer-x"}}, + "serviceCatalog": [ + {"endpoints": [ + {"adminURL": "http://swift.admin-nets.local:8080/", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8080/v1/AUTH_1", + "publicURL": + "http://swift.publicinternets.com/v1/AUTH_1"}], + "type": "object-store", + "name": "swift"}, + {"endpoints": [ + {"adminURL": "http://cdn.admin-nets.local/v1.1/1", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:7777/v1.1/1", + "publicURL": + "http://cdn.publicinternets.com/v1.1/1"}], + "type": "object-store", + "name": "cdn"}], + "user": + {"id": "1", + "roles": [ + {"tenantId": "1", + "id": "3", + "name": "Member"}], + "name": "joeuser"}} + } + ] + return (200, body) + + def get_tokens_887665443383838(self, **kw): + body = [ + {"access": + {"token": + {"expires": "2012-02-05T00:00:00", + "id": "887665443383838", + "tenant": {"id": "1", + "name": "customer-x"}}, + "user": + {"name": "joeuser", + "tenantName": "customer-x", + "id": "1", + "roles": [{"serviceId": "1", + "id": "3", + "name": "Member"}], + "tenantId": "1"}} + } + ] + return (200, body) + + def get_tokens_887665443383838_endpoints(self, **kw): + body = [ + {"endpoints_links": [ + {"href": + "http://127.0.0.1:35357/tokens/887665443383838" + "/endpoints?'marker=5&limit=10'", + "rel": "next"}], + "endpoints": [ + {"internalURL": "http://127.0.0.1:8080/v1/AUTH_1", + "name": "swift", + "adminURL": "http://swift.admin-nets.local:8080/", + "region": "RegionOne", + "tenantId": 1, + "type": "object-store", + "id": 1, + "publicURL": "http://swift.publicinternets.com/v1/AUTH_1"}, + {"internalURL": "http://localhost:8774/v1.0", + "name": "nova_compat", + "adminURL": "http://127.0.0.1:8774/v1.0", + "region": "RegionOne", + "tenantId": 1, + "type": "compute", + "id": 2, + "publicURL": "http://nova.publicinternets.com/v1.0/"}, + {"internalURL": "http://localhost:8774/v1.1", + "name": "nova", + "adminURL": "http://127.0.0.1:8774/v1.1", + "region": "RegionOne", + "tenantId": 1, + "type": "compute", + "id": 3, + "publicURL": "http://nova.publicinternets.com/v1.1/"}, + {"internalURL": "http://127.0.0.1:9292/v1.1/", + "name": "glance", + "adminURL": "http://nova.admin-nets.local/v1.1/", + "region": "RegionOne", + "tenantId": 1, + "type": "image", + "id": 4, + "publicURL": "http://glance.publicinternets.com/v1.1/"}, + {"internalURL": "http://127.0.0.1:7777/v1.1/1", + "name": "cdn", + "adminURL": "http://cdn.admin-nets.local/v1.1/1", + "region": "RegionOne", + "tenantId": 1, + "versionId": "1.1", + "versionList": "http://127.0.0.1:7777/", + "versionInfo": "http://127.0.0.1:7777/v1.1", + "type": "object-store", + "id": 5, + "publicURL": "http://cdn.publicinternets.com/v1.1/1"}] + } + ] + return (200, body) + + def get(self, **kw): + body = { + "version": { + "id": "v2.0", + "status": "beta", + "updated": "2011-11-19T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:35357/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:35357/v2.0/identity-admin.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"}] + } + } + return (200, body) + + def get_extensions(self, **kw): + body = { + "extensions": {"values": []} + } + return (200, body) + + def post_tenants(self, **kw): + body = {"tenant": + {"enabled": True, + "description": None, + "name": "new-tenant", + "id": "1"}} + return (200, body) + + def post_tenants_2(self, **kw): + body = {"tenant": + {"enabled": False, + "description": "desc", + "name": "new-tenant1", + "id": "2"}} + return (200, body) + + def get_tenants(self, **kw): + body = { + "tenants_links": [], + "tenants": [ + {"enabled": False, + "description": None, + "name": "project-y", + "id": "1"}, + {"enabled": True, + "description": None, + "name": "new-tenant", + "id": "2"}, + {"enabled": True, + "description": None, + "name": "customer-x", + "id": "1"}] + } + return (200, body) + + def get_tenants_1(self, **kw): + body = {"tenant": + {"enabled": True, + "description": None, + "name": "new-tenant", + "id": "1"}} + return (200, body) + + def get_tenants_2(self, **kw): + body = {"tenant": + {"enabled": True, + "description": None, + "name": "new-tenant", + "id": "2"}} + return (200, body) + + def delete_tenants_2(self, **kw): + body = {} + return (200, body) + + def get_tenants_1_users_1_roles(self, **kw): + body = { + "roles": [ + {"id": "1", + "name": "Admin"}, + {"id": "2", + "name": "Member"}, + {"id": "3", + "name": "new-role"}] + } + return (200, body) + + def put_users_1_roles_OS_KSADM_1(self, **kw): + body = { + "roles": + {"id": "1", + "name": "Admin"}} + return (200, body) + + def delete_users_1_roles_OS_KSADM_1(self, **kw): + body = {} + return (200, body) + + def put_tenants_1_users_1_roles_OS_KSADM_1(self, **kw): + body = { + "role": + {"id": "1", + "name": "Admin"}} + return (200, body) + + def get_users(self, **kw): + body = { + "users": [ + {"name": self.username, + "enabled": "true", + "email": "sdfsdf@sdfsd.sdf", + "id": "1", + "tenantId": "1"}, + {"name": "user2", + "enabled": "true", + "email": "sdfsdf@sdfsd.sdf", + "id": "2", + "tenantId": "1"}] + } + return (200, body) + + def get_users_1(self, **kw): + body = { + "user": { + "tenantId": "1", + "enabled": "true", + "id": "1", + "name": self.username} + } + return (200, body) + + def put_users_1(self, **kw): + body = { + "user": { + "tenantId": "1", + "enabled": "true", + "id": "1", + "name": "new-user1", + "email": "user@email.com"} + } + return (200, body) + + def put_users_1_OS_KSADM_password(self, **kw): + body = { + "user": { + "tenantId": "1", + "enabled": "true", + "id": "1", + "name": "new-user1", + "email": "user@email.com"} + } + return (200, body) + + def post_users(self, **kw): + body = { + "user": { + "tenantId": "1", + "enabled": "true", + "id": "1", + "name": self.username} + } + return (200, body) + + def delete_users_1(self, **kw): + body = [] + return (200, body) + + def get_users_1_roles(self, **kw): + body = [ + {"roles_links": [], + "roles":[ + {"id": "2", + "name": "KeystoneServiceAdmin"}] + } + ] + return (200, body) + + def post_OS_KSADM_roles(self, **kw): + body = {"role": + {"name": "new-role", + "id": "1"}} + return (200, body) + + def get_OS_KSADM_roles(self, **kw): + body = {"roles": [ + {"id": "10", "name": "admin"}, + {"id": "20", "name": "member"}, + {"id": "1", "name": "new-role"}] + } + return (200, body) + + def get_OS_KSADM_roles_1(self, **kw): + body = {"role": + {"name": "new-role", + "id": "1"} + } + return (200, body) + + def delete_OS_KSADM_roles_1(self, **kw): + body = {} + return (200, body) + + def post_OS_KSADM_services(self, **kw): + body = {"OS-KSADM:service": + {"id": "1", + "type": "compute", + "name": "service1", + "description": None} + } + return (200, body) + + def get_OS_KSADM_services_1(self, **kw): + body = {"OS-KSADM:service": + {"description": None, + "type": "compute", + "id": "1", + "name": "service1"} + } + return (200, body) + + def get_OS_KSADM_services(self, **kw): + body = { + "OS-KSADM:services": [ + {"description": None, + "type": "compute", + "id": "1", + "name": "service1"}, + {"description": None, + "type": "identity", + "id": "2", + "name": "service2"}] + } + return (200, body) + + def delete_OS_KSADM_services_1(self, **kw): + body = {} + return (200, body) + + def post_users_1_credentials_OS_EC2(self, **kw): + body = {"credential": + {"access": "1", + "tenant_id": "1", + "secret": "1", + "user_id": "1"} + } + return (200, body) + + def get_users_1_credentials_OS_EC2(self, **kw): + body = {"credentials": [ + {"access": "1", + "tenant_id": "1", + "secret": "1", + "user_id": "1"}] + } + return (200, body) + + def get_users_1_credentials_OS_EC2_2(self, **kw): + body = { + "credential": + {"access": "2", + "tenant_id": "1", + "secret": "1", + "user_id": "1"} + } + return (200, body) + + def delete_users_1_credentials_OS_EC2_2(self, **kw): + body = {} + return (200, body) + + def patch_OS_KSCRUD_users_1(self, **kw): + body = {} + return (200, body) + + def get_endpoints(self, **kw): + body = { + 'endpoints': [ + {'adminURL': 'http://cdn.admin-nets.local/v1.1/1', + 'region': 'RegionOne', + 'internalURL': 'http://127.0.0.1:7777/v1.1/1', + 'publicURL': 'http://cdn.publicinternets.com/v1.1/1'}], + 'type': 'compute', + 'name': 'nova-compute' + } + return (200, body) + + def post_endpoints(self, **kw): + body = { + "endpoint": + {"adminURL": "http://swift.admin-nets.local:8080/", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8080/v1/AUTH_1", + "publicURL": "http://swift.publicinternets.com/v1/AUTH_1"}, + "type": "compute", + "name": "nova-compute" + } + return (200, body) diff --git a/keystonemiddleware/tests/v2_0/test_access.py b/keystonemiddleware/tests/v2_0/test_access.py new file mode 100644 index 00000000..2982eb9d --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_access.py @@ -0,0 +1,135 @@ +# 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 testresources + +from keystoneclient import access +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests import client_fixtures as token_data +from keystoneclient.tests.v2_0 import client_fixtures +from keystoneclient.tests.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_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) + + 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): + auth_ref = access.AccessInfo.factory( + body=client_fixtures.project_scoped_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_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 load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystonemiddleware/tests/v2_0/test_auth.py b/keystonemiddleware/tests/v2_0/test_auth.py new file mode 100644 index 00000000..62607e1e --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_auth.py @@ -0,0 +1,274 @@ +# 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 json + +import httpretty +import six + +from keystoneclient import exceptions +from keystoneclient.openstack.common import jsonutils +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests.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, + }, + } + + @httpretty.activate + def test_authenticate_success_expired(self): + # Build an expired token + self.TEST_RESPONSE_DICT['access']['token']['expires'] = ( + (timeutils.utcnow() - datetime.timedelta(1)).isoformat()) + + exp_resp = httpretty.Response(body=json.dumps(self.TEST_RESPONSE_DICT), + content_type='application/json') + + # Build a new response + TEST_TOKEN = "abcdef" + self.TEST_RESPONSE_DICT['access']['token']['expires'] = ( + '2020-01-01T00:00:10.000123Z') + self.TEST_RESPONSE_DICT['access']['token']['id'] = TEST_TOKEN + + new_resp = httpretty.Response(body=json.dumps(self.TEST_RESPONSE_DICT), + content_type='application/json') + + # return expired first, and then the new response + self.stub_auth(responses=[exp_resp, new_resp]) + + 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) + + @httpretty.activate + 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=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) + + @httpretty.activate + def test_auth_redirect(self): + self.stub_auth(status=305, body='Use Proxy', + 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) + + @httpretty.activate + 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) + + @httpretty.activate + 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) + + @httpretty.activate + 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 = httpretty.last_request().body + if six.PY3: + body = body.decode('utf-8') + body = jsonutils.loads(body) + self.assertEqual(body['auth']['token']['id'], fake_token) + + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + self.assertEqual(httpretty.last_request().headers.get('X-Auth-Token'), + self.TEST_TOKEN) + + @httpretty.activate + 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) + + @httpretty.activate + 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.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + 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) + + @httpretty.activate + 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) + + self.assertEqual(httpretty.last_request().headers.get('X-Auth-Token'), + self.TEST_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) + + self.assertEqual(httpretty.last_request().headers.get('X-Auth-Token'), + fake_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) + + self.assertEqual(httpretty.last_request().headers.get('X-Auth-Token'), + self.TEST_TOKEN) diff --git a/keystonemiddleware/tests/v2_0/test_client.py b/keystonemiddleware/tests/v2_0/test_client.py new file mode 100644 index 00000000..fac0a5ba --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_client.py @@ -0,0 +1,162 @@ +# 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 httpretty + +from keystoneclient import exceptions +from keystoneclient import fixture +from keystoneclient.tests.v2_0 import client_fixtures +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import client + + +class KeystoneClientTest(utils.TestCase): + + @httpretty.activate + def test_unscoped_init(self): + self.stub_auth(json=client_fixtures.unscoped_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) + + @httpretty.activate + def test_scoped_init(self): + self.stub_auth(json=client_fixtures.project_scoped_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) + + @httpretty.activate + 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') + + @httpretty.activate + 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') + + @httpretty.activate + 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(json=first) + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + cl.authenticate() + self.assertEqual(cl.management_url, admin_url) + + self.stub_auth(json=second) + cl.authenticate() + self.assertEqual(cl.management_url, second_url) + + @httpretty.activate + 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) diff --git a/keystonemiddleware/tests/v2_0/test_discovery.py b/keystonemiddleware/tests/v2_0/test_discovery.py new file mode 100644 index 00000000..a3192c9d --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_discovery.py @@ -0,0 +1,83 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from keystoneclient.generic import client +from keystoneclient.tests.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", + }], + }], + }, + } + + @httpretty.activate + def test_get_versions(self): + self.stub_url(httpretty.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']) + + @httpretty.activate + def test_get_version_local(self): + self.stub_url(httpretty.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/keystonemiddleware/tests/v2_0/test_ec2.py b/keystonemiddleware/tests/v2_0/test_ec2.py new file mode 100644 index 00000000..dbdaadd3 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_ec2.py @@ -0,0 +1,113 @@ +# 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 httpretty + +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import ec2 + + +class EC2Tests(utils.TestCase): + + @httpretty.activate + 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(httpretty.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) + + @httpretty.activate + 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(httpretty.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') + + @httpretty.activate + 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(httpretty.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') + + @httpretty.activate + def test_delete(self): + user_id = 'usr' + access = 'access' + self.stub_url(httpretty.DELETE, ['users', user_id, 'credentials', + 'OS-EC2', access], status=204) + self.client.ec2.delete(user_id, access) diff --git a/keystonemiddleware/tests/v2_0/test_endpoints.py b/keystonemiddleware/tests/v2_0/test_endpoints.py new file mode 100644 index 00000000..66513eb3 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_endpoints.py @@ -0,0 +1,86 @@ +# 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 httpretty + +from keystoneclient.tests.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', + } + ] + } + + @httpretty.activate + def test_create(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": "e044e21", + } + } + + resp_body = { + "endpoint": { + "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "region": "RegionOne", + "id": "1fd485b2ffd54f409a5ecd42cba11401", + "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + } + } + + self.stub_url(httpretty.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) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['endpoints', '8f953'], status=204) + self.client.endpoints.delete('8f953') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.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/keystonemiddleware/tests/v2_0/test_extensions.py b/keystonemiddleware/tests/v2_0/test_extensions.py new file mode 100644 index 00000000..6f4787d9 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_extensions.py @@ -0,0 +1,66 @@ +# 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 httpretty + +from keystoneclient.tests.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"}]', + } + ] + } + } + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.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/keystonemiddleware/tests/v2_0/test_roles.py b/keystonemiddleware/tests/v2_0/test_roles.py new file mode 100644 index 00000000..b896c753 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_roles.py @@ -0,0 +1,132 @@ +# 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 httpretty + +from keystoneclient.tests.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, + } + ], + }, + } + + @httpretty.activate + 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(httpretty.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']) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, + ['OS-KSADM', 'roles', self.ADMIN_ROLE_ID], status=204) + self.client.roles.delete(self.ADMIN_ROLE_ID) + + @httpretty.activate + def test_get(self): + self.stub_url(httpretty.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') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.GET, ['OS-KSADM', 'roles'], + json=self.TEST_ROLES) + + role_list = self.client.roles.list() + [self.assertIsInstance(r, roles.Role) for r in role_list] + + @httpretty.activate + def test_roles_for_user(self): + self.stub_url(httpretty.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] + + @httpretty.activate + def test_roles_for_user_tenant(self): + self.stub_url(httpretty.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] + + @httpretty.activate + def test_add_user_role(self): + self.stub_url(httpretty.PUT, ['users', 'foo', 'roles', 'OS-KSADM', + 'barrr'], status=204) + + self.client.roles.add_user_role('foo', 'barrr') + + @httpretty.activate + def test_add_user_role_tenant(self): + id_ = uuid.uuid4().hex + self.stub_url(httpretty.PUT, ['tenants', id_, 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], status=204) + + self.client.roles.add_user_role('foo', 'barrr', id_) + + @httpretty.activate + def test_remove_user_role(self): + self.stub_url(httpretty.DELETE, ['users', 'foo', 'roles', 'OS-KSADM', + 'barrr'], status=204) + self.client.roles.remove_user_role('foo', 'barrr') + + @httpretty.activate + def test_remove_user_role_tenant(self): + id_ = uuid.uuid4().hex + self.stub_url(httpretty.DELETE, ['tenants', id_, 'users', 'foo', + 'roles', 'OS-KSADM', 'barrr'], + status=204) + self.client.roles.remove_user_role('foo', 'barrr', id_) diff --git a/keystonemiddleware/tests/v2_0/test_service_catalog.py b/keystonemiddleware/tests/v2_0/test_service_catalog.py new file mode 100644 index 00000000..bc219fd4 --- /dev/null +++ b/keystonemiddleware/tests/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.v2_0 import client_fixtures +from keystoneclient.tests.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/keystonemiddleware/tests/v2_0/test_services.py b/keystonemiddleware/tests/v2_0/test_services.py new file mode 100644 index 00000000..79a19ebe --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_services.py @@ -0,0 +1,105 @@ +# 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 httpretty + +from keystoneclient.tests.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 + }, + ], + }, + } + + @httpretty.activate + 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(httpretty.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) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, + ['OS-KSADM', 'services', self.NOVA_SERVICE_ID], + status=204) + + self.client.services.delete(self.NOVA_SERVICE_ID) + + @httpretty.activate + def test_get(self): + test_services = self.TEST_SERVICES['OS-KSADM:services']['values'][0] + + self.stub_url(httpretty.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') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.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/keystonemiddleware/tests/v2_0/test_shell.py b/keystonemiddleware/tests/v2_0/test_shell.py new file mode 100644 index 00000000..54ca0d43 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_shell.py @@ -0,0 +1,361 @@ +# 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 mox3 import stubout +import six +from testtools import matchers + +from keystoneclient import httpclient +from keystoneclient.tests.v2_0 import fakes +from keystoneclient.tests.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/' + + +class ShellTests(utils.TestCase): + + def setUp(self): + """Patch os.environ to avoid required auth info.""" + + super(ShellTests, self).setUp() + self.stubs = stubout.StubOutForTesting() + + self.fake_client = fakes.FakeHTTPClient() + self.stubs.Set( + httpclient.HTTPClient, "_cs_request", + lambda ign_self, *args, **kwargs: + self.fake_client._cs_request(*args, **kwargs)) + self.stubs.Set( + httpclient.HTTPClient, "authenticate", + lambda cl_obj: + self.fake_client.authenticate(cl_obj)) + 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() + + def tearDown(self): + self.stubs.UnsetAll() + self.stubs.SmartUnsetAll() + os.environ = self.old_environment + self.fake_client.clear_callstack() + 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, url, body=None, **kwargs): + return self.fake_client.assert_called(method, url, body, **kwargs) + + def assert_called_anytime(self, method, url, body=None): + return self.fake_client.assert_called_anytime(method, url, body) + + def test_user_list(self): + self.run_command('user-list') + self.fake_client.assert_called_anytime('GET', '/users') + + def test_user_create(self): + self.run_command('user-create --name new-user') + self.fake_client.assert_called_anytime( + 'POST', '/users', + {'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): + with mock.patch('getpass.getpass') as mock_getpass: + mock_getpass.return_value = 'newpass' + self.run_command('user-create --name new-user --pass') + self.fake_client.assert_called_anytime( + 'POST', '/users', + {'user': + {'email': None, + 'password': 'newpass', + 'enabled': True, + 'name': 'new-user', + 'tenantId': None}}) + + def test_user_get(self): + self.run_command('user-get 1') + self.fake_client.assert_called_anytime('GET', '/users/1') + + def test_user_delete(self): + self.run_command('user-delete 1') + self.fake_client.assert_called_anytime('DELETE', '/users/1') + + def test_user_password_update(self): + self.run_command('user-password-update --pass newpass 1') + self.fake_client.assert_called_anytime( + 'PUT', '/users/1/OS-KSADM/password') + + def test_user_update(self): + self.run_command('user-update --name new-user1' + ' --email user@email.com --enabled true 1') + self.fake_client.assert_called_anytime( + 'PUT', '/users/1', + {'user': + {'id': '1', + 'email': 'user@email.com', + 'enabled': True, + 'name': 'new-user1'} + }) + 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.fake_client.assert_called_anytime( + 'PUT', '/users/1', + {'user': + {'id': '1', + 'email': ''} + }) + + def test_role_create(self): + self.run_command('role-create --name new-role') + self.fake_client.assert_called_anytime( + 'POST', '/OS-KSADM/roles', + {"role": {"name": "new-role"}}) + + def test_role_get(self): + self.run_command('role-get 1') + self.fake_client.assert_called_anytime('GET', '/OS-KSADM/roles/1') + + def test_role_list(self): + self.run_command('role-list') + self.fake_client.assert_called_anytime('GET', '/OS-KSADM/roles') + + def test_role_delete(self): + self.run_command('role-delete 1') + self.fake_client.assert_called_anytime('DELETE', '/OS-KSADM/roles/1') + + def test_user_role_add(self): + self.run_command('user-role-add --user_id 1 --role_id 1') + self.fake_client.assert_called_anytime( + 'PUT', '/users/1/roles/OS-KSADM/1') + + def test_user_role_list(self): + self.run_command('user-role-list --user_id 1 --tenant-id 1') + self.fake_client.assert_called_anytime( + 'GET', '/tenants/1/users/1/roles') + self.run_command('user-role-list --user_id 1') + self.fake_client.assert_called_anytime( + 'GET', '/tenants/1/users/1/roles') + self.run_command('user-role-list') + self.fake_client.assert_called_anytime( + 'GET', '/tenants/1/users/1/roles') + + def test_user_role_remove(self): + self.run_command('user-role-remove --user_id 1 --role_id 1') + self.fake_client.assert_called_anytime( + 'DELETE', '/users/1/roles/OS-KSADM/1') + + def test_tenant_create(self): + self.run_command('tenant-create --name new-tenant') + self.fake_client.assert_called_anytime( + 'POST', '/tenants', + {"tenant": {"enabled": True, + "name": "new-tenant", + "description": None}}) + + def test_tenant_get(self): + self.run_command('tenant-get 2') + self.fake_client.assert_called_anytime('GET', '/tenants/2') + + def test_tenant_list(self): + self.run_command('tenant-list') + self.fake_client.assert_called_anytime('GET', '/tenants') + + def test_tenant_update(self): + self.run_command('tenant-update' + ' --name new-tenant1 --enabled false' + ' --description desc 2') + self.fake_client.assert_called_anytime( + 'POST', '/tenants/2', + {"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.run_command('tenant-delete 2') + self.fake_client.assert_called_anytime('DELETE', '/tenants/2') + + def test_service_create(self): + self.run_command('service-create --name service1 --type compute') + self.fake_client.assert_called_anytime( + 'POST', '/OS-KSADM/services', + {"OS-KSADM:service": + {"type": "compute", + "name": "service1", + "description": None}}) + + def test_service_get(self): + self.run_command('service-get 1') + self.fake_client.assert_called_anytime('GET', '/OS-KSADM/services/1') + + def test_service_list(self): + self.run_command('service-list') + self.fake_client.assert_called_anytime('GET', '/OS-KSADM/services') + + def test_service_delete(self): + self.run_command('service-delete 1') + self.fake_client.assert_called_anytime( + '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.run_command('ec2-credentials-create' + ' --tenant-id 1 --user-id 1') + self.fake_client.assert_called_anytime( + 'POST', '/users/1/credentials/OS-EC2', + {'tenant_id': '1'}) + + self.run_command('ec2-credentials-create --tenant-id 1') + self.fake_client.assert_called_anytime( + 'POST', '/users/1/credentials/OS-EC2', + {'tenant_id': '1'}) + + self.run_command('ec2-credentials-create') + self.fake_client.assert_called_anytime( + 'POST', '/users/1/credentials/OS-EC2', + {'tenant_id': '1'}) + + def test_ec2_credentials_delete(self): + self.run_command('ec2-credentials-delete --access 2 --user-id 1') + self.fake_client.assert_called_anytime( + 'DELETE', '/users/1/credentials/OS-EC2/2') + + self.run_command('ec2-credentials-delete --access 2') + self.fake_client.assert_called_anytime( + 'DELETE', '/users/1/credentials/OS-EC2/2') + + def test_ec2_credentials_list(self): + self.run_command('ec2-credentials-list --user-id 1') + self.fake_client.assert_called_anytime( + 'GET', '/users/1/credentials/OS-EC2') + + self.run_command('ec2-credentials-list') + self.fake_client.assert_called_anytime( + 'GET', '/users/1/credentials/OS-EC2') + + def test_ec2_credentials_get(self): + self.run_command('ec2-credentials-get --access 2 --user-id 1') + self.fake_client.assert_called_anytime( + 'GET', '/users/1/credentials/OS-EC2/2') + + def test_bootstrap(self): + self.run_command('bootstrap --user-name new-user' + ' --pass 1 --role-name admin' + ' --tenant-name new-tenant') + self.fake_client.assert_called_anytime( + 'POST', '/users', + {'user': + {'email': None, + 'password': '1', + 'enabled': True, + 'name': 'new-user', + 'tenantId': None}}) + self.run_command('bootstrap --user-name new-user' + ' --pass 1 --role-name admin' + ' --tenant-name new-tenant') + self.fake_client.assert_called_anytime( + 'POST', '/tenants', + {"tenant": {"enabled": True, + "name": "new-tenant", + "description": None}}) + self.run_command('bootstrap --user-name new-user' + ' --pass 1 --role-name new-role' + ' --tenant-name new-tenant') + self.fake_client.assert_called_anytime( + 'POST', '/OS-KSADM/roles', + {"role": {"name": "new-role"}}) + + self.run_command('bootstrap --user-name' + ' new-user --pass 1 --role-name admin' + ' --tenant-name new-tenant') + self.fake_client.assert_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.run_command('password-update --current-password oldpass' + ' --new-password newpass') + self.fake_client.assert_called_anytime( + 'PATCH', '/OS-KSCRUD/users/1', + {'user': + {'original_password': 'oldpass', + 'password': 'newpass'}}) + + def test_endpoint_create(self): + self.run_command('endpoint-create --service-id 1 ' + '--publicurl=http://example.com:1234/go') + self.fake_client.assert_called_anytime( + 'POST', '/endpoints', + {'endpoint': + {'adminurl': None, + 'service_id': '1', + 'region': 'regionOne', + 'internalurl': None, + 'publicurl': "http://example.com:1234/go"}}) + + def test_endpoint_list(self): + self.run_command('endpoint-list') + self.fake_client.assert_called_anytime('GET', '/endpoints') diff --git a/keystonemiddleware/tests/v2_0/test_tenants.py b/keystonemiddleware/tests/v2_0/test_tenants.py new file mode 100644 index 00000000..f366e00d --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_tenants.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 httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import tenants + + +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": [], + }, + } + + @httpretty.activate + 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(httpretty.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) + + @httpretty.activate + 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(httpretty.POST, ['tenants'], status=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) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['tenants', self.ADMIN_ID], status=204) + self.client.tenants.delete(self.ADMIN_ID) + + @httpretty.activate + def test_get(self): + resp = {'tenant': self.TEST_TENANTS['tenants']['values'][2]} + self.stub_url(httpretty.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') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.GET, ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list() + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + @httpretty.activate + def test_list_limit(self): + self.stub_url(httpretty.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] + + @httpretty.activate + def test_list_marker(self): + self.stub_url(httpretty.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] + + @httpretty.activate + def test_list_limit_marker(self): + self.stub_url(httpretty.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] + + @httpretty.activate + 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(httpretty.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") + + @httpretty.activate + 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(httpretty.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) + + @httpretty.activate + def test_add_user(self): + self.stub_url(httpretty.PUT, + ['tenants', self.EXTRAS_ID, 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], + status=204) + + self.client.tenants.add_user(self.EXTRAS_ID, 'foo', 'barrr') + + @httpretty.activate + def test_remove_user(self): + self.stub_url(httpretty.DELETE, ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status=204) + + self.client.tenants.remove_user(self.EXTRAS_ID, 'foo', 'barrr') + + @httpretty.activate + def test_tenant_add_user(self): + self.stub_url(httpretty.PUT, ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status=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) + + @httpretty.activate + def test_tenant_remove_user(self): + self.stub_url(httpretty.DELETE, ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status=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) diff --git a/keystonemiddleware/tests/v2_0/test_tokens.py b/keystonemiddleware/tests/v2_0/test_tokens.py new file mode 100644 index 00000000..bd7e6cf2 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_tokens.py @@ -0,0 +1,25 @@ +# 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 httpretty + +from keystoneclient.tests.v2_0 import utils + + +class TokenTests(utils.TestCase): + @httpretty.activate + def test_delete(self): + id_ = uuid.uuid4().hex + self.stub_url(httpretty.DELETE, ['tokens', id_], status=204) + self.client.tokens.delete(id_) diff --git a/keystonemiddleware/tests/v2_0/test_users.py b/keystonemiddleware/tests/v2_0/test_users.py new file mode 100644 index 00000000..3b660db0 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/test_users.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 httpretty + +from keystoneclient.tests.v2_0 import utils +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", + }, + ] + } + } + + @httpretty.activate + def test_create(self): + tenant_id = uuid.uuid4().hex + user_id = uuid.uuid4().hex + req_body = { + "user": { + "name": "gabriel", + "password": "test", + "tenantId": tenant_id, + "email": "test@example.com", + "enabled": True, + } + } + + resp_body = { + "user": { + "name": "gabriel", + "enabled": True, + "tenantId": tenant_id, + "id": user_id, + "password": "test", + "email": "test@example.com", + } + } + + self.stub_url(httpretty.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) + + @httpretty.activate + 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(httpretty.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) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['users', self.ADMIN_USER_ID], + status=204) + self.client.users.delete(self.ADMIN_USER_ID) + + @httpretty.activate + def test_get(self): + self.stub_url(httpretty.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') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.GET, ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list() + [self.assertIsInstance(u, users.User) for u in user_list] + + @httpretty.activate + def test_list_limit(self): + self.stub_url(httpretty.GET, ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(limit=1) + self.assertEqual(httpretty.last_request().querystring, + {'limit': ['1']}) + [self.assertIsInstance(u, users.User) for u in user_list] + + @httpretty.activate + def test_list_marker(self): + self.stub_url(httpretty.GET, ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(marker='foo') + self.assertDictEqual(httpretty.last_request().querystring, + {'marker': ['foo']}) + [self.assertIsInstance(u, users.User) for u in user_list] + + @httpretty.activate + def test_list_limit_marker(self): + self.stub_url(httpretty.GET, ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(limit=1, marker='foo') + + self.assertDictEqual(httpretty.last_request().querystring, + {'marker': ['foo'], 'limit': ['1']}) + [self.assertIsInstance(u, users.User) for u in user_list] + + @httpretty.activate + def test_update(self): + req_1 = { + "user": { + "id": self.DEMO_USER_ID, + "email": "gabriel@example.com", + "name": "gabriel", + } + } + req_2 = { + "user": { + "id": self.DEMO_USER_ID, + "password": "swordfish", + } + } + 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(httpretty.PUT, ['users', self.DEMO_USER_ID], json=req_1) + self.stub_url(httpretty.PUT, + ['users', self.DEMO_USER_ID, 'OS-KSADM', 'password'], + json=req_2) + self.stub_url(httpretty.PUT, + ['users', self.DEMO_USER_ID, 'OS-KSADM', 'tenant'], + json=req_3) + self.stub_url(httpretty.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, 'swordfish') + 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) + + @httpretty.activate + def test_update_own_password(self): + req_body = { + 'user': { + 'password': 'ABCD', 'original_password': 'DCBA' + } + } + resp_body = { + 'access': {} + } + user_id = uuid.uuid4().hex + self.stub_url(httpretty.PATCH, ['OS-KSCRUD', 'users', user_id], + json=resp_body) + + self.client.user_id = user_id + self.client.users.update_own_password('DCBA', 'ABCD') + self.assertRequestBodyIs(json=req_body) diff --git a/keystonemiddleware/tests/v2_0/utils.py b/keystonemiddleware/tests/v2_0/utils.py new file mode 100644 index 00000000..f4c89114 --- /dev/null +++ b/keystonemiddleware/tests/v2_0/utils.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 httpretty + +from keystoneclient.tests 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(httpretty.POST, ['tokens'], **kwargs) diff --git a/keystonemiddleware/tests/v3/__init__.py b/keystonemiddleware/tests/v3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystonemiddleware/tests/v3/client_fixtures.py b/keystonemiddleware/tests/v3/client_fixtures.py new file mode 100644 index 00000000..6764257d --- /dev/null +++ b/keystonemiddleware/tests/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/keystonemiddleware/tests/v3/test_access.py b/keystonemiddleware/tests/v3/test_access.py new file mode 100644 index 00000000..cae09f97 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_access.py @@ -0,0 +1,146 @@ +# 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 + +from keystoneclient import access +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests.v3 import client_fixtures +from keystoneclient.tests.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_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'])) + + 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_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_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) diff --git a/keystonemiddleware/tests/v3/test_auth.py b/keystonemiddleware/tests/v3/test_auth.py new file mode 100644 index 00000000..71edd33e --- /dev/null +++ b/keystonemiddleware/tests/v3/test_auth.py @@ -0,0 +1,369 @@ +# 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 httpretty +import six + +from keystoneclient import exceptions +from keystoneclient.openstack.common import jsonutils +from keystoneclient.tests.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 + } + + @httpretty.activate + 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) + + @httpretty.activate + 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=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) + + @httpretty.activate + def test_auth_redirect(self): + self.stub_auth(status=305, body='Use proxy', + location=self.TEST_ADMIN_URL + '/auth/tokens') + + 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"]) + + @httpretty.activate + 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"]) + + @httpretty.activate + 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) + + @httpretty.activate + 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) + + @httpretty.activate + 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) + + @httpretty.activate + 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 = httpretty.last_request().body + if six.PY3: + body = body.decode('utf-8') + body = jsonutils.loads(body) + self.assertEqual(body['auth']['identity']['token']['id'], fake_token) + + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + self.assertEqual(httpretty.last_request().headers.get('X-Auth-Token'), + self.TEST_TOKEN) + + @httpretty.activate + 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) + + @httpretty.activate + 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) + + @httpretty.activate + 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) + + @httpretty.activate + 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) + + self.assertEqual(httpretty.last_request().headers.get('X-Auth-Token'), + self.TEST_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) + + self.assertEqual(httpretty.last_request().headers.get('X-Auth-Token'), + fake_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) + + self.assertEqual(httpretty.last_request().headers.get('X-Auth-Token'), + self.TEST_TOKEN) diff --git a/keystonemiddleware/tests/v3/test_client.py b/keystonemiddleware/tests/v3/test_client.py new file mode 100644 index 00000000..cef8fbab --- /dev/null +++ b/keystonemiddleware/tests/v3/test_client.py @@ -0,0 +1,207 @@ +# 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 httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.v3 import client_fixtures +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import client + + +class KeystoneClientTest(utils.TestCase): + + @httpretty.activate + 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') + + @httpretty.activate + 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') + + @httpretty.activate + 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') + + @httpretty.activate + 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') + + @httpretty.activate + 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') + + @httpretty.activate + 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.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(json=fixture) + cl = client.Client(username='exampleuser', + password='password', + auth_url=self.TEST_URL, + **kwargs) + + self.assertEqual(cl.management_url, first_url) + + self.stub_auth(json=second) + cl.authenticate() + self.assertEqual(cl.management_url, second_url % 35357) + + @httpretty.activate + def test_management_url_is_updated_with_project(self): + self._management_url_is_updated(client_fixtures.project_scoped_token(), + project_name='exampleproject') + + @httpretty.activate + def test_management_url_is_updated_with_domain(self): + self._management_url_is_updated(client_fixtures.domain_scoped_token(), + domain_name='exampledomain') + + @httpretty.activate + 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) diff --git a/keystonemiddleware/tests/v3/test_credentials.py b/keystonemiddleware/tests/v3/test_credentials.py new file mode 100644 index 00000000..d6ad4555 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_credentials.py @@ -0,0 +1,53 @@ +# 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.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) + 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/keystonemiddleware/tests/v3/test_discover.py b/keystonemiddleware/tests/v3/test_discover.py new file mode 100644 index 00000000..19be82d0 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_discover.py @@ -0,0 +1,85 @@ +# 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 httpretty + +from keystoneclient.generic import client +from keystoneclient.tests.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', + } + + @httpretty.activate + def test_get_version_local(self): + httpretty.register_uri(httpretty.GET, "http://localhost:35357/", + status=300, + body=json.dumps(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/keystonemiddleware/tests/v3/test_domains.py b/keystonemiddleware/tests/v3/test_domains.py new file mode 100644 index 00000000..e86971ac --- /dev/null +++ b/keystonemiddleware/tests/v3/test_domains.py @@ -0,0 +1,43 @@ +# 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.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) diff --git a/keystonemiddleware/tests/v3/test_endpoint_filter.py b/keystonemiddleware/tests/v3/test_endpoint_filter.py new file mode 100644 index 00000000..f3e283c3 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_endpoint_filter.py @@ -0,0 +1,153 @@ +# 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 + +import httpretty + +from keystoneclient.tests.v3 import utils + + +class EndpointFilterTests(utils.TestCase): + """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_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 + + 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 + + @httpretty.activate + def test_add_endpoint_to_project_via_id(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url(httpretty.PUT, + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status=201) + + self.manager.add_endpoint_to_project(project=project_id, + endpoint=endpoint_id) + + @httpretty.activate + 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(httpretty.PUT, + [self.manager.OS_EP_FILTER_EXT, + 'projects', project_ref['id'], + 'endpoints', endpoint_ref['id']], + status=201) + + self.manager.add_endpoint_to_project(project=project, + endpoint=endpoint) + + @httpretty.activate + def test_delete_endpoint_from_project(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url(httpretty.DELETE, + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status=201) + + self.manager.delete_endpoint_from_project(project=project_id, + endpoint=endpoint_id) + + @httpretty.activate + def test_check_endpoint_in_project(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url(httpretty.HEAD, + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status=201) + + self.manager.check_endpoint_in_project(project=project_id, + endpoint=endpoint_id) + + @httpretty.activate + 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(httpretty.GET, + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints'], + json=endpoints, + status=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) + + @httpretty.activate + 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(httpretty.GET, + [self.manager.OS_EP_FILTER_EXT, 'endpoints', endpoint_id, + 'projects'], + json=projects, + status=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/keystonemiddleware/tests/v3/test_endpoints.py b/keystonemiddleware/tests/v3/test_endpoints.py new file mode 100644 index 00000000..5319373e --- /dev/null +++ b/keystonemiddleware/tests/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.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/keystonemiddleware/tests/v3/test_federation.py b/keystonemiddleware/tests/v3/test_federation.py new file mode 100644 index 00000000..f16cf2eb --- /dev/null +++ b/keystonemiddleware/tests/v3/test_federation.py @@ -0,0 +1,127 @@ +# 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 httpretty + +from keystoneclient.tests.v3 import utils +from keystoneclient.v3.contrib.federation import identity_providers +from keystoneclient.v3.contrib.federation import mappings + + +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) + + @httpretty.activate + 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(httpretty.PUT, entity=ref, id=ref['id'], status=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 + + @httpretty.activate + 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(httpretty.PUT, entity=req_ref, id=mapping_id, + status=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) diff --git a/keystonemiddleware/tests/v3/test_groups.py b/keystonemiddleware/tests/v3/test_groups.py new file mode 100644 index 00000000..74a4a4ca --- /dev/null +++ b/keystonemiddleware/tests/v3/test_groups.py @@ -0,0 +1,63 @@ +# 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 + +import httpretty + +from keystoneclient.tests.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 + + @httpretty.activate + def test_list_groups_for_user(self): + user_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity(httpretty.GET, + ['users', user_id, self.collection_key], + status=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] + + @httpretty.activate + def test_list_groups_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + domain_id = uuid.uuid4().hex + + self.stub_entity(httpretty.GET, + [self.collection_key], + status=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.assertEqual(httpretty.last_request().querystring, + {'domain_id': [domain_id]}) diff --git a/keystonemiddleware/tests/v3/test_oauth1.py b/keystonemiddleware/tests/v3/test_oauth1.py new file mode 100644 index 00000000..a422c3d8 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_oauth1.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 + +import httpretty +import mock +import six +from testtools import matchers + +from keystoneclient.openstack.common import jsonutils +from keystoneclient.openstack.common import timeutils +from keystoneclient import session +from keystoneclient.tests.v3 import client_fixtures +from keystoneclient.tests.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 + + @httpretty.activate + def test_description_is_optional(self): + consumer_id = uuid.uuid4().hex + resp_ref = {'consumer': {'description': None, + 'id': consumer_id}} + + self.stub_url(httpretty.POST, + [self.path_prefix, self.collection_key], + status=201, json=resp_ref) + + consumer = self.manager.create() + self.assertEqual(consumer_id, consumer.id) + self.assertIsNone(consumer.description) + + @httpretty.activate + def test_description_not_included(self): + consumer_id = uuid.uuid4().hex + resp_ref = {'consumer': {'id': consumer_id}} + + self.stub_url(httpretty.POST, + [self.path_prefix, self.collection_key], + status=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 + token = 'oauth_token=%s&oauth_token_secret=%s' % (key, secret) + return (key, secret, token) + + def _new_oauth_token_with_expires_at(self): + key, secret, token = self._new_oauth_token() + expires_at = timeutils.strtime() + token += '&oauth_expires_at=%s' % expires_at + 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' + + @httpretty.activate + 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(httpretty.PUT, + [self.path_prefix, 'authorize', request_key], + status=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) + + @httpretty.activate + 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() + + # NOTE(stevemar) The server expects the body to be JSON. Even though + # the resp_ref is a string it is not a JSON string. + self.stub_url(httpretty.POST, [self.path_prefix, 'request_token'], + status=201, body=jsonutils.dumps(resp_ref), + content_type='application/x-www-form-urlencoded') + + # 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 = httpretty.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' + + @httpretty.activate + 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 + + # NOTE(stevemar) The server expects the body to be JSON. Even though + # the resp_ref is a string it is not a JSON string. + self.stub_url(httpretty.POST, [self.path_prefix, 'access_token'], + status=201, body=jsonutils.dumps(resp_ref), + content_type='application/x-www-form-urlencoded') + + # 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 = httpretty.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') + + @httpretty.activate + 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 = httpretty.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/keystonemiddleware/tests/v3/test_policies.py b/keystonemiddleware/tests/v3/test_policies.py new file mode 100644 index 00000000..45bce719 --- /dev/null +++ b/keystonemiddleware/tests/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.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/keystonemiddleware/tests/v3/test_projects.py b/keystonemiddleware/tests/v3/test_projects.py new file mode 100644 index 00000000..1d4b2d7c --- /dev/null +++ b/keystonemiddleware/tests/v3/test_projects.py @@ -0,0 +1,62 @@ +# 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 httpretty + +from keystoneclient.tests.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) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + @httpretty.activate + def test_list_projects_for_user(self): + ref_list = [self.new_ref(), self.new_ref()] + user_id = uuid.uuid4().hex + + self.stub_entity(httpretty.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] + + @httpretty.activate + def test_list_projects_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + domain_id = uuid.uuid4().hex + + self.stub_entity(httpretty.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.assertEqual(httpretty.last_request().querystring, + {'domain_id': [domain_id]}) diff --git a/keystonemiddleware/tests/v3/test_regions.py b/keystonemiddleware/tests/v3/test_regions.py new file mode 100644 index 00000000..c539aa75 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_regions.py @@ -0,0 +1,33 @@ +# 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.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 diff --git a/keystonemiddleware/tests/v3/test_role_assignments.py b/keystonemiddleware/tests/v3/test_role_assignments.py new file mode 100644 index 00000000..a28024e4 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_role_assignments.py @@ -0,0 +1,220 @@ +# 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 httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.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] + + @httpretty.activate + def test_list_params(self): + ref_list = self.TEST_USER_PROJECT_LIST + self.stub_entity(httpretty.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) + + @httpretty.activate + def test_all_assignments_list(self): + ref_list = self.TEST_ALL_RESPONSE_LIST + self.stub_entity(httpretty.GET, + [self.collection_key], + entity=ref_list) + + returned_list = self.manager.list() + self._assert_returned_list(ref_list, returned_list) + + kwargs = {} + self.assertQueryStringContains(**kwargs) + + @httpretty.activate + def test_project_assignments_list(self): + ref_list = self.TEST_GROUP_PROJECT_LIST + self.TEST_USER_PROJECT_LIST + self.stub_entity(httpretty.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) + + @httpretty.activate + def test_domain_assignments_list(self): + ref_list = self.TEST_USER_DOMAIN_LIST + self.stub_entity(httpretty.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) + + @httpretty.activate + def test_group_assignments_list(self): + ref_list = self.TEST_GROUP_PROJECT_LIST + self.stub_entity(httpretty.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) + + @httpretty.activate + def test_user_assignments_list(self): + ref_list = self.TEST_USER_DOMAIN_LIST + self.TEST_USER_PROJECT_LIST + self.stub_entity(httpretty.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) + + @httpretty.activate + def test_effective_assignments_list(self): + ref_list = self.TEST_USER_PROJECT_LIST + self.TEST_USER_DOMAIN_LIST + self.stub_entity(httpretty.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) + + @httpretty.activate + def test_role_assignments_list(self): + ref_list = self.TEST_ALL_RESPONSE_LIST + self.stub_entity(httpretty.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/keystonemiddleware/tests/v3/test_roles.py b/keystonemiddleware/tests/v3/test_roles.py new file mode 100644 index 00000000..9e3b8177 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_roles.py @@ -0,0 +1,350 @@ +# 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 + +import httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.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 + + @httpretty.activate + def test_domain_role_grant(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.PUT, + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status=201) + + self.manager.grant(role=ref['id'], domain=domain_id, user=user_id) + + @httpretty.activate + def test_domain_group_role_grant(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.PUT, + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status=201) + + self.manager.grant(role=ref['id'], domain=domain_id, group=group_id) + + @httpretty.activate + 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(httpretty.GET, + ['domains', domain_id, 'users', user_id, + self.collection_key], entity=ref_list) + + self.manager.list(domain=domain_id, user=user_id) + + @httpretty.activate + 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(httpretty.GET, + ['domains', domain_id, 'groups', group_id, + self.collection_key], entity=ref_list) + + self.manager.list(domain=domain_id, group=group_id) + + @httpretty.activate + def test_domain_role_check(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status=204) + + self.manager.check(role=ref['id'], domain=domain_id, + user=user_id) + + @httpretty.activate + 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(httpretty.HEAD, + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status=204) + + self.manager.check(role=ref['id'], domain=domain_id, group=group_id) + + @httpretty.activate + def test_domain_role_revoke(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, user=user_id) + + @httpretty.activate + def test_domain_group_role_revoke(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, group=group_id) + + @httpretty.activate + def test_project_role_grant(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.PUT, + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status=201) + + self.manager.grant(role=ref['id'], project=project_id, user=user_id) + + @httpretty.activate + def test_project_group_role_grant(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.PUT, + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status=201) + + self.manager.grant(role=ref['id'], project=project_id, group=group_id) + + @httpretty.activate + 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(httpretty.GET, + ['projects', project_id, 'users', user_id, + self.collection_key], entity=ref_list) + + self.manager.list(project=project_id, user=user_id) + + @httpretty.activate + 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(httpretty.GET, + ['projects', project_id, 'groups', group_id, + self.collection_key], entity=ref_list) + + self.manager.list(project=project_id, group=group_id) + + @httpretty.activate + def test_project_role_check(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status=200) + + self.manager.check(role=ref['id'], project=project_id, user=user_id) + + @httpretty.activate + def test_project_group_role_check(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status=200) + + self.manager.check(role=ref['id'], project=project_id, group=group_id) + + @httpretty.activate + def test_project_role_revoke(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status=204) + + self.manager.revoke(role=ref['id'], project=project_id, user=user_id) + + @httpretty.activate + def test_project_group_role_revoke(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status=204) + + self.manager.revoke(role=ref['id'], project=project_id, group=group_id) + + @httpretty.activate + 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/keystonemiddleware/tests/v3/test_service_catalog.py b/keystonemiddleware/tests/v3/test_service_catalog.py new file mode 100644 index 00000000..7a96c3a2 --- /dev/null +++ b/keystonemiddleware/tests/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.v3 import client_fixtures +from keystoneclient.tests.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/keystonemiddleware/tests/v3/test_services.py b/keystonemiddleware/tests/v3/test_services.py new file mode 100644 index 00000000..d271afe0 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_services.py @@ -0,0 +1,32 @@ +# 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.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 diff --git a/keystonemiddleware/tests/v3/test_trusts.py b/keystonemiddleware/tests/v3/test_trusts.py new file mode 100644 index 00000000..15e93488 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_trusts.py @@ -0,0 +1,108 @@ +# 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.openstack.common import timeutils +from keystoneclient.tests.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() + + # 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() + + # 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() + 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/keystonemiddleware/tests/v3/test_users.py b/keystonemiddleware/tests/v3/test_users.py new file mode 100644 index 00000000..153e27a6 --- /dev/null +++ b/keystonemiddleware/tests/v3/test_users.py @@ -0,0 +1,251 @@ +# 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 + +import httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.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 + + @httpretty.activate + def test_add_user_to_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + self.stub_url(httpretty.PUT, + ['groups', group_id, self.collection_key, ref['id']], + status=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) + + @httpretty.activate + def test_list_users_in_group(self): + group_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity(httpretty.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] + + @httpretty.activate + def test_check_user_in_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['groups', group_id, self.collection_key, ref['id']], + status=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) + + @httpretty.activate + def test_remove_user_from_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['groups', group_id, self.collection_key, ref['id']], + status=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) + + @httpretty.activate + 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(httpretty.POST, [self.collection_key], + status=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) + + @httpretty.activate + 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(httpretty.POST, + [self.collection_key], + status=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) + + @httpretty.activate + 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(httpretty.PATCH, + [self.collection_key, ref['id']], + status=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) + + @httpretty.activate + 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(httpretty.PATCH, + [self.collection_key, ref['id']], + status=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) + + @httpretty.activate + def test_update_password(self): + old_password = uuid.uuid4().hex + new_password = uuid.uuid4().hex + + self.stub_url(httpretty.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('/v3/users/test/password', + httpretty.last_request().path) + self.assertRequestBodyIs(json=exp_req_body) + + 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/keystonemiddleware/tests/v3/utils.py b/keystonemiddleware/tests/v3/utils.py new file mode 100644 index 00000000..dbc0f06e --- /dev/null +++ b/keystonemiddleware/tests/v3/utils.py @@ -0,0 +1,327 @@ +# 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 httpretty +import six +from six.moves.urllib import parse as urlparse + +from keystoneclient.openstack.common import jsonutils +from keystoneclient.tests 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 + + self.stub_url(httpretty.POST, ['auth', 'tokens'], + X_Subject_Token=subject_token, **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)) + + @httpretty.activate + 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) + req_ref = (req_ref or ref).copy() + req_ref.pop('id') + + self.stub_entity(httpretty.POST, entity=req_ref, status=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) + + @httpretty.activate + def test_get(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity(httpretty.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 + + @httpretty.activate + 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) + + httpretty.register_uri(httpretty.GET, + urlparse.urljoin(self.TEST_URL, expected_path), + body=jsonutils.dumps(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] + + # register_uri doesn't match the querystring component, so we have to + # explicitly test the querystring component passed by the manager + qs_args = httpretty.last_request().querystring + qs_args_expected = expected_query or filter_kwargs + for key, value in six.iteritems(qs_args_expected): + self.assertIn(key, qs_args) + # The httppretty.querystring value is a list + # Note we convert the value to a string, 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), 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) + + @httpretty.activate + def test_list_params(self): + ref_list = [self.new_ref()] + filter_kwargs = {uuid.uuid4().hex: uuid.uuid4().hex} + expected_path = self._get_expected_path() + + httpretty.register_uri(httpretty.GET, + urlparse.urljoin(self.TEST_URL, expected_path), + body=jsonutils.dumps(self.encode(ref_list))) + + self.manager.list(**filter_kwargs) + self.assertQueryStringContains(**filter_kwargs) + + @httpretty.activate + def test_find(self, ref=None): + ref = ref or self.new_ref() + ref_list = [ref] + + self.stub_entity(httpretty.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('') + + @httpretty.activate + def test_update(self, ref=None, req_ref=None): + ref = ref or self.new_ref() + + self.stub_entity(httpretty.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) + req_ref = (req_ref or 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) + + @httpretty.activate + def test_delete(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity(httpretty.DELETE, id=ref['id'], status=204) + self.manager.delete(ref['id'])