diff --git a/storyboard/api/auth/__init__.py b/storyboard/api/auth/__init__.py index e5d49343..72454900 100644 --- a/storyboard/api/auth/__init__.py +++ b/storyboard/api/auth/__init__.py @@ -62,7 +62,6 @@ class ErrorMessages(object): INVALID_NO_EMAIL = _('Please permit access to your email address.') INVALID_NO_NAME = _('Please permit access to your name.') - INVALID_NO_NICKNAME = _('Please permit access to your nickname.') INVALID_TOKEN_GRANT_TYPE = _('Only grant types "authorization_code" and ' '"refresh_token" are supported.') diff --git a/storyboard/api/auth/oauth_validator.py b/storyboard/api/auth/oauth_validator.py index feb694fb..b3faca43 100644 --- a/storyboard/api/auth/oauth_validator.py +++ b/storyboard/api/auth/oauth_validator.py @@ -112,12 +112,10 @@ class SkeletonValidator(RequestValidator): openid = request._params["openid.claimed_id"] email = request._params["openid.sreg.email"] full_name = request._params["openid.sreg.fullname"] - username = request._params["openid.sreg.nickname"] last_login = datetime.datetime.now(pytz.utc) user = user_api.user_get_by_openid(openid) user_dict = {"full_name": full_name, - "username": username, "email": email, "last_login": last_login} diff --git a/storyboard/api/auth/openid_client.py b/storyboard/api/auth/openid_client.py index 2388c497..ee7cd40b 100644 --- a/storyboard/api/auth/openid_client.py +++ b/storyboard/api/auth/openid_client.py @@ -108,7 +108,7 @@ class OpenIdClient(object): "openid.return_to": return_to_url, "openid.ns.sreg": "http://openid.net/sreg/1.0", - "openid.sreg.required": "fullname,email,nickname", + "openid.sreg.required": "fullname,email", "openid.ns.ext2": "http://openid.net/srv/ax/1.0", "openid.ext2.mode": "fetch_request", @@ -146,7 +146,6 @@ class OpenIdClient(object): required_parameters = { 'openid.sreg.email': e_msg.INVALID_NO_EMAIL, 'openid.sreg.fullname': e_msg.INVALID_NO_NAME, - 'openid.sreg.nickname': e_msg.INVALID_NO_NICKNAME, } for name, error in six.iteritems(required_parameters): diff --git a/storyboard/api/v1/search/search_engine.py b/storyboard/api/v1/search/search_engine.py index 406072ca..114241d8 100644 --- a/storyboard/api/v1/search/search_engine.py +++ b/storyboard/api/v1/search/search_engine.py @@ -41,7 +41,7 @@ class SearchEngine(object): models.Story: ["title", "description"], models.Task: ["title"], models.Comment: ["content"], - models.User: ['username', 'full_name', 'email'] + models.User: ['full_name', 'email'] } @abc.abstractmethod diff --git a/storyboard/api/v1/users.py b/storyboard/api/v1/users.py index 729f05c6..f26798c4 100644 --- a/storyboard/api/v1/users.py +++ b/storyboard/api/v1/users.py @@ -59,7 +59,7 @@ class UsersController(rest.RestController): @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.User], int, int, wtypes.text, wtypes.text, wtypes.text, wtypes.text) - def get(self, marker=None, limit=None, username=None, full_name=None, + def get(self, marker=None, limit=None, full_name=None, sort_field='id', sort_dir='asc'): """Page and filter the users in storyboard. @@ -80,12 +80,11 @@ class UsersController(rest.RestController): marker_user = users_api.user_get(marker) users = users_api.user_get_all(marker=marker_user, limit=limit, - username=username, full_name=full_name, + full_name=full_name, filter_non_public=True, sort_field=sort_field, sort_dir=sort_dir) - user_count = users_api.user_get_count(username=username, - full_name=full_name) + user_count = users_api.user_get_count(full_name=full_name) # Apply the query response headers. response.headers['X-Limit'] = str(limit) diff --git a/storyboard/api/v1/validations.py b/storyboard/api/v1/validations.py index 629ae6d8..26b0c5ca 100644 --- a/storyboard/api/v1/validations.py +++ b/storyboard/api/v1/validations.py @@ -22,11 +22,6 @@ USERS_PUT_SCHEMA = { "name": "user_schema", "type": "object", "properties": { - "username": { - "type": "string", - "minLength": CommonLength.lower_middle_length, - "maxLength": CommonLength.name_length - }, "full_name": { "type": ["string"], "minLength": CommonLength.lower_middle_length, @@ -45,7 +40,7 @@ USERS_PUT_SCHEMA = { } USERS_POST_SCHEMA = copy.deepcopy(USERS_PUT_SCHEMA) -USERS_POST_SCHEMA["required"] = ["username", "full_name", "email"] +USERS_POST_SCHEMA["required"] = ["full_name", "email"] USER_PREFERENCES_POST_SCHEMA = { "name": "userPreference_schema", diff --git a/storyboard/api/v1/wmodels.py b/storyboard/api/v1/wmodels.py index 9c0599c0..397a576e 100644 --- a/storyboard/api/v1/wmodels.py +++ b/storyboard/api/v1/wmodels.py @@ -360,10 +360,6 @@ class TimeLineEvent(base.APIBase): class User(base.APIBase): """Represents a user.""" - username = wtypes.text - """A short unique name, beginning with a lower-case letter or number, and - containing only letters, numbers, dots, hyphens, or plus signs""" - full_name = wtypes.text """Full (Display) name.""" @@ -385,7 +381,6 @@ class User(base.APIBase): @classmethod def sample(cls): return cls( - username="elbarto", full_name="Bart Simpson", openid="https://login.launchpad.net/+id/Abacaba", email="skinnerstinks@springfield.net", diff --git a/storyboard/common/user_utils.py b/storyboard/common/user_utils.py deleted file mode 100644 index 682dfcc6..00000000 --- a/storyboard/common/user_utils.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2014 Mirantis Inc. -# -# 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 storyboard.db.api import users as users_api - - -def username_by_id(user_id): - user = users_api.user_get(user_id) - username = user.full_name or user.username - return username diff --git a/storyboard/db/migration/alembic_migrations/versions/042_remove_nick.py b/storyboard/db/migration/alembic_migrations/versions/042_remove_nick.py new file mode 100644 index 00000000..8bbde8c5 --- /dev/null +++ b/storyboard/db/migration/alembic_migrations/versions/042_remove_nick.py @@ -0,0 +1,63 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""This migration removes the 'nickname' column from the user table. + +Revision ID: 042 +Revises: 041 +Create Date: 2015-02-17 12:00:00 + +""" + +# revision identifiers, used by Alembic. + +revision = '042' +down_revision = '041' + +from alembic import op +from oslo_log import log +import sqlalchemy as sa + +LOG = log.getLogger(__name__) + + +def upgrade(active_plugins=None, options=None): + op.drop_column('users', 'username') + + # Handle the FT Index on the user table. + version_info = op.get_bind().engine.dialect.server_version_info + if version_info[0] < 5 or version_info[0] == 5 and version_info[1] < 6: + LOG.warn("MySQL version is lower than 5.6. Skipping full-text indexes") + return + + # Index for users + op.drop_index("users_fti", table_name='users') + op.execute("ALTER TABLE users " + "ADD FULLTEXT users_fti (full_name, email)") + + +def downgrade(active_plugins=None, options=None): + op.add_column( + 'users', + sa.Column('username', sa.Unicode(length=30), nullable=True), + ) + + version_info = op.get_bind().engine.dialect.server_version_info + if version_info[0] < 5 or version_info[0] == 5 and version_info[1] < 6: + LOG.warn("MySQL version is lower than 5.6. Skipping full-text indexes") + return + + # Index for users + op.drop_index("users_fti", table_name='users') + op.execute("ALTER TABLE users " + "ADD FULLTEXT users_fti (username, full_name, email)") diff --git a/storyboard/db/models.py b/storyboard/db/models.py index 00784391..a71b51b2 100644 --- a/storyboard/db/models.py +++ b/storyboard/db/models.py @@ -140,9 +140,8 @@ class User(FullText, ModelBuilder, Base): schema.UniqueConstraint('email', name='uniq_user_email'), ) - __fulltext_columns__ = ['username', 'full_name', 'email'] + __fulltext_columns__ = ['full_name', 'email'] - username = Column(Unicode(CommonLength.name_length)) full_name = Column(Unicode(CommonLength.top_large_length), nullable=True) email = Column(String(CommonLength.top_large_length)) openid = Column(String(CommonLength.top_large_length)) @@ -156,7 +155,7 @@ class User(FullText, ModelBuilder, Base): preferences = relationship("UserPreference") - _public_fields = ["id", "openid", "full_name", "username", "last_login", + _public_fields = ["id", "openid", "full_name", "last_login", "enable_login"] diff --git a/storyboard/migrate/launchpad/writer.py b/storyboard/migrate/launchpad/writer.py index 9f7aa643..ebc03fe2 100644 --- a/storyboard/migrate/launchpad/writer.py +++ b/storyboard/migrate/launchpad/writer.py @@ -97,7 +97,6 @@ class LaunchpadWriter(object): if lp_user is None: return lp_user - username = lp_user.name display_name = lp_user.display_name user_link = lp_user.web_link @@ -114,9 +113,10 @@ class LaunchpadWriter(object): except DiscoveryFailure: # If we encounter a launchpad maintenance user, # give it an invalid openid. - print "WARNING: Invalid OpenID for user \'%s\'" % (username,) + print "WARNING: Invalid OpenID for user \'%s\'" \ + % (display_name,) self._openid_map[user_link] = \ - 'http://example.com/invalid/~%s' % (username,) + 'http://example.com/invalid/~%s' % (display_name,) openid = self._openid_map[user_link] @@ -131,10 +131,9 @@ class LaunchpadWriter(object): # Use a temporary email address, since LP won't give this to # us and it'll be updated on first login anyway. user = users_api.user_create({ - 'username': username, 'openid': openid, 'full_name': display_name, - 'email': "%s@example.com" % (username) + 'email': "%s@example.com" % (display_name) }) self._user_map[openid] = user diff --git a/storyboard/tests/api/auth/test_oauth.py b/storyboard/tests/api/auth/test_oauth.py index fdc9afac..932d14f4 100644 --- a/storyboard/tests/api/auth/test_oauth.py +++ b/storyboard/tests/api/auth/test_oauth.py @@ -121,7 +121,6 @@ class TestOAuthAuthorize(BaseOAuthTest): # Check OAuth Registration parameters self.assertIn('fullname', parameters['openid.sreg.required'][0]) self.assertIn('email', parameters['openid.sreg.required'][0]) - self.assertIn('nickname', parameters['openid.sreg.required'][0]) # Check redirect URL redirect = parameters['openid.return_to'][0] @@ -348,10 +347,9 @@ class TestOAuthAuthorizeReturn(BaseOAuthTest): "ax.type.FirstName,ax.type.LastName,claimed_id," "identity,mode,ns,ns.ax,ns.sreg,op_endpoint," "response_nonce,return_to,signed,sreg.email," - "sreg.fullname,sreg.nickname", + "sreg.fullname", "openid.sreg.email": "test@example.com", "openid.sreg.fullname": "Test User", - "openid.sreg.nickname": "superuser" } def _mock_response(self, mock_post, valid=True): @@ -473,34 +471,6 @@ class TestOAuthAuthorizeReturn(BaseOAuthTest): error='invalid_request', error_description=e_msg.INVALID_NO_EMAIL) - def test_invalid_redirect_no_username(self, mock_post): - """If the oauth response to storyboard is valid, but does not include a - first name, it should error. - - TODO: Remove during work for - https://storyboard.openstack.org/#!/story/2000152 - """ - self._mock_response(mock_post, valid=True) - - random_state = six.text_type(uuid.uuid4()) - - invalid_params = self.valid_params.copy() - del invalid_params['openid.sreg.nickname'] - - # Simple GET with various parameters - response = self.get_json(path='/openid/authorize_return', - expect_errors=True, - state=random_state, - **invalid_params) - - # Validate the redirect response - self.assertValidRedirect(response=response, - expected_status_code=302, - redirect_uri= - self.valid_params['sb_redirect_uri'], - error='invalid_request', - error_description=e_msg.INVALID_NO_NICKNAME) - class TestOAuthAccessToken(BaseOAuthTest): """Functional test for the /oauth/token endpoint for the generation of diff --git a/storyboard/tests/api/test_db_exceptions.py b/storyboard/tests/api/test_db_exceptions.py index 8ee94e3e..cd3e7324 100644 --- a/storyboard/tests/api/test_db_exceptions.py +++ b/storyboard/tests/api/test_db_exceptions.py @@ -47,14 +47,12 @@ class TestDBExceptions(base.FunctionalTest): # send user first time resource = '/users' user = { - 'username': 'test_duplicate', 'full_name': 'Test duplicate', 'email': 'dupe@example.com' } response = self.post_json(resource, user) users_body = response.json - self.assertEqual(user['username'], users_body['username']) self.assertEqual(user['full_name'], users_body['full_name']) self.assertEqual(user['email'], users_body['email']) diff --git a/storyboard/tests/api/test_jsonschema.py b/storyboard/tests/api/test_jsonschema.py index f6a09f45..3eacfffb 100644 --- a/storyboard/tests/api/test_jsonschema.py +++ b/storyboard/tests/api/test_jsonschema.py @@ -65,17 +65,15 @@ class TestUsers(base.FunctionalTest): self.default_headers['Authorization'] = 'Bearer valid_superuser_token' self.user_01 = { - 'username': 'jsonschema_test_user1', 'full_name': 'jsonschema_test_user1', 'email': 'jsonschema_test_user1@test.ru', 'openid': 'qwerty' } self.user_02 = { - 'username': 't2', - 'full_name': 'jsonschema_test_user2', - 'email': 'jsonschema_test_user2@test.ru', - 'openid': 'qwertyu' + 'full_name': LONG_STRING, + 'email': 'jsonschema_test_user3@test.ru', + 'openid': 'qwertyui' } self.user_03 = { @@ -85,12 +83,6 @@ class TestUsers(base.FunctionalTest): 'openid': 'qwertyui' } - self.user_04 = { - 'full_name': 'jsonschema_test_user4', - 'email': 'jsonschema_test_user4@test.ru', - 'openid': 'qwertyuio' - } - self.put_user_01 = { 'full_name': 'new full_name of regular User' } @@ -107,9 +99,7 @@ class TestUsers(base.FunctionalTest): create(self, self.user_01, self.resource) def test_create_invalid(self): - create_invalid_length(self, self.user_02, self.resource, 'username') - create_invalid_length(self, self.user_03, self.resource, 'full_name') - create_invalid_required(self, self.user_04, self.resource, 'username') + create_invalid_length(self, self.user_02, self.resource, 'full_name') def test_update(self): resource = "".join([self.resource, "/2"]) diff --git a/storyboard/tests/api/test_users.py b/storyboard/tests/api/test_users.py index bd482cf0..5933a477 100644 --- a/storyboard/tests/api/test_users.py +++ b/storyboard/tests/api/test_users.py @@ -87,5 +87,5 @@ class TestSearchUsers(base.FunctionalTest): self.default_headers['Authorization'] = 'Bearer valid_user_token' def testBrowse(self): - result = self.get_json(self.resource + '?username=regularuser') + result = self.get_json(self.resource + '?full_name=Regular') self.assertEqual(1, len(result)) diff --git a/storyboard/tests/mock_data.py b/storyboard/tests/mock_data.py index ee10c1c8..3a3bb7cd 100644 --- a/storyboard/tests/mock_data.py +++ b/storyboard/tests/mock_data.py @@ -40,19 +40,16 @@ def load(): # Load users load_data([ User(id=1, - username='superuser', email='superuser@example.com', openid='superuser_openid', full_name='Super User', is_superuser=True), User(id=2, - username='regularuser', email='regularuser@example.com', openid='regularuser_openid', full_name='Regular User', is_superuser=False), User(id=3, - username='otheruser', email='otheruser@example.com', openid='otheruser_openid', full_name='Other User',