From 6f8bdae1dbe19eb3eb346fb696506b4390f320fb Mon Sep 17 00:00:00 2001 From: David Stanek Date: Wed, 25 Sep 2013 13:29:33 +0000 Subject: [PATCH] Adds support for username to match the v2 spec The v2.0 API spec uses username for the user's name, whereas the v3 API just uses name. The v2.0 implementaion incorrectly used name instead of username, but did allow a username to be specified and stored in the extras. This patch makes the implementation more closely conform to the API without breaking backward compatibility. Anyone using name in the v2.0 API can continue to do so. They can even specify a username that will still get stored in the extras. Users can now use the documented username instead of name and the API will work for them as well. Both name and username will always be returned for the v2 API calls. DocImpact Change-Id: Ia95aa5d442a8311925399fa59e5022d31f68d374 Closes-Bug: #1214686 --- keystone/common/controller.py | 24 +++++ keystone/identity/controllers.py | 2 + keystone/identity/core.py | 2 + keystone/tests/test_content_types.py | 155 +++++++++++++++++++++++++++ keystone/tests/test_v3_identity.py | 4 +- 5 files changed, 186 insertions(+), 1 deletion(-) diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 9c0f5b0c91..e2f057c6ae 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -313,6 +313,30 @@ class V2Controller(wsgi.Application): ref.pop('domain_id', None) return ref + @staticmethod + def normalize_username_in_response(ref): + """Adds username to outgoing user refs to match the v2 spec. + + Internally we use `name` to represent a user's name. The v2 spec + requires the use of `username` instead. + + """ + if 'username' not in ref and 'name' in ref: + ref['username'] = ref['name'] + return ref + + @staticmethod + def normalize_username_in_request(ref): + """Adds name in incoming user refs to match the v2 spec. + + Internally we use `name` to represent a user's name. The v2 spec + requires the use of `username` instead. + + """ + if 'name' not in ref and 'username' in ref: + ref['name'] = ref.pop('username') + return ref + class V3Controller(V2Controller): """Base controller class for Identity API v3. diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 2bba0e3839..23b50aa62a 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -192,6 +192,7 @@ class User(controller.V2Controller): # CRUD extension def create_user(self, context, user): user = self._normalize_OSKSADM_password_on_request(user) + user = self.normalize_username_in_request(user) user = self._normalize_dict(user) self.assert_admin(context) @@ -221,6 +222,7 @@ class User(controller.V2Controller): def update_user(self, context, user_id, user): # NOTE(termie): this is really more of a patch than a put + user = self.normalize_username_in_request(user) self.assert_admin(context) if 'enabled' in user and not isinstance(user['enabled'], bool): diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 6a9f90c9d9..adb23b2eaa 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -213,6 +213,7 @@ class Manager(manager.Manager): * v2.0 users are not domain aware, and should have domain_id removed * v2.0 users expect the use of tenantId instead of default_project_id + * v2.0 users have a username attribute This method should only be applied to user_refs being returned from the v2.0 controller(s). @@ -237,6 +238,7 @@ class Manager(manager.Manager): """Run through the various filter/normalization methods.""" _format_default_project_id(ref) controller.V2Controller.filter_domain_id(ref) + controller.V2Controller.normalize_username_in_response(ref) return ref if isinstance(ref, dict): diff --git a/keystone/tests/test_content_types.py b/keystone/tests/test_content_types.py index d065ea121f..46b18054d7 100644 --- a/keystone/tests/test_content_types.py +++ b/keystone/tests/test_content_types.py @@ -783,6 +783,161 @@ class LegacyV2UsernameTests(object): user = self.get_user_from_response(r) self.assertEqual(user.get('username'), 'new_username') + def test_username_is_always_returned_create(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + r = self.create_user() + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_is_always_returned_get(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + r = self.admin_request(path='/v2.0/users/%s' % id_, token=token) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_is_always_returned_get_by_name(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + token = self.get_scoped_token() + + r = self.create_user() + + name = self.get_user_attribute_from_response(r, 'name') + r = self.admin_request(path='/v2.0/users?name=%s' % name, token=token) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_is_always_returned_update_no_username_provided(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + name = self.get_user_attribute_from_response(r, 'name') + enabled = self.get_user_attribute_from_response(r, 'enabled') + r = self.admin_request( + method='PUT', + path='/v2.0/users/%s' % id_, + token=token, + body={ + 'user': { + 'name': name, + 'enabled': enabled, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_updated_username_is_returned(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + name = self.get_user_attribute_from_response(r, 'name') + enabled = self.get_user_attribute_from_response(r, 'enabled') + r = self.admin_request( + method='PUT', + path='/v2.0/users/%s' % id_, + token=token, + body={ + 'user': { + 'name': name, + 'enabled': enabled, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_can_be_used_instead_of_name_create(self): + token = self.get_scoped_token() + + r = self.admin_request( + method='POST', + path='/v2.0/users', + token=token, + body={ + 'user': { + 'username': uuid.uuid4().hex, + 'enabled': True, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_can_be_used_instead_of_name_update(self): + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + new_username = uuid.uuid4().hex + enabled = self.get_user_attribute_from_response(r, 'enabled') + r = self.admin_request( + method='PUT', + path='/v2.0/users/%s' % id_, + token=token, + body={ + 'user': { + 'username': new_username, + 'enabled': enabled, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), new_username) + self.assertEqual(user.get('name'), user.get('username')) + class RestfulTestCase(rest.RestfulTestCase): diff --git a/keystone/tests/test_v3_identity.py b/keystone/tests/test_v3_identity.py index 551a96489c..7490ef73cf 100644 --- a/keystone/tests/test_v3_identity.py +++ b/keystone/tests/test_v3_identity.py @@ -1623,11 +1623,13 @@ class TestV3toV2Methods(tests.TestCase): # Expected result if the user is meant to have a tenantId element self.expected_user = {'id': self.user_id, 'name': self.user_id, + 'username': self.user_id, 'tenantId': self.default_project_id} # Expected result if the user is not meant ot have a tenantId element self.expected_user_no_tenant_id = {'id': self.user_id, - 'name': self.user_id} + 'name': self.user_id, + 'username': self.user_id} def test_v3_to_v2_user_method(self):