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):