Merge lp:~cprov/canonical-identity-provider/cleaner-usernames into lp:canonical-identity-provider/release

Proposed by Celso Providelo
Status: Merged
Approved by: Celso Providelo
Approved revision: no longer in the source branch.
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: lp:~cprov/canonical-identity-provider/cleaner-usernames
Merge into: lp:canonical-identity-provider/release
Diff against target: 172 lines (+88/-11)
4 files modified
src/identityprovider/forms.py (+10/-4)
src/identityprovider/tests/test_forms.py (+55/-0)
src/identityprovider/tests/test_views_server.py (+1/-1)
src/webui/tests/test_views_account.py (+22/-6)
To merge this branch: bzr merge lp:~cprov/canonical-identity-provider/cleaner-usernames
Reviewer Review Type Date Requested Status
Daniel Manrique (community) Approve
Review via email: mp+365370@code.launchpad.net

Commit message

Stop accepting usernames with DOTs and PLUSes.

To post a comment you must log in.
Revision history for this message
Daniel Manrique (roadmr) wrote :

LGTM - my only concern is - this will force users with non-compliant usernames to update them if they want to edit their account details, correct? since VALID_NAME_RE is used in EditAccountForm's clean_username method. Do we know about how many users would be in this situation? and - since this username is tied to Launchpad's username, could this result in a situation where a user is completely blocked from performing account changes? example: someone with a non-compliant name and a published PPA will be unable to change their username due to Launchpad policy which could prevent them from e.g. changing their password.

That's about it, the code looks reasonable vs. the intent described in the Trello card.

review: Approve
Revision history for this message
Celso Providelo (cprov) wrote :

As discussed on IRC, users with now bad-usernames and PPAs will be locked in the SSO account edit form until we come up with a solution the resembles "usernames aliases" in LP.

Revision history for this message
Celso Providelo (cprov) wrote :

Oops, the regexp is missing some requirements (>=3 chars & refuse digit-only terms)

Revision history for this message
Daniel Manrique (roadmr) wrote :

+1 with two nitpicks and two possible test string suggestions.

review: Approve
Revision history for this message
Celso Providelo (cprov) :
Revision history for this message
Daniel Manrique (roadmr) wrote :

LGTM thanks!

review: Approve
Revision history for this message
Igor Komlew (glower) wrote :

With this regexp you can have more separators after each other, like: x----x----x

Revision history for this message
Daniel Manrique (roadmr) wrote :

The description is ambiguous as to whether multiple consecutive separators are allowed, so a decision has to be made. It was:

roadmr niemeyer: is 'x---x---x' a valid username? that's all we need to know
niemeyer roadmr: No

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/identityprovider/forms.py'
2--- src/identityprovider/forms.py 2018-12-13 14:47:48 +0000
3+++ src/identityprovider/forms.py 2019-04-03 17:04:50 +0000
4@@ -54,12 +54,13 @@
5 ),
6 }
7
8-VALID_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9\+\.\-]+$")
9+# Should match SCA ``short_namespace_re` expression.
10+VALID_NAME_RE = re.compile(r"(?!^[\d-]+$)^[a-z\d](-?[a-z\d]){2,31}$")
11
12 INVALID_NAME = _(
13- 'Usernames must be at least two characters long and start with a letter '
14- 'or number. All letters must be lower-case. The characters +, - and . '
15- 'are also allowed after the first character.')
16+ 'Usernames must be 3 to 32 characters long, using lower-case letters or '
17+ 'digits and the character - can be used as a separator. Numbers-only '
18+ 'terms and consecutive separators are not allowed.')
19
20
21 class DeleteAccountForm(forms.Form):
22@@ -336,6 +337,11 @@
23 username = self.cleaned_data['username'].strip()
24 if not username:
25 return username
26+ # Previously set usernames can skip verification. That allows
27+ # users to edit their account details even if their usernames
28+ # are now considered "distasteful".
29+ if username == self.instance.person_name:
30+ return username
31 if not VALID_NAME_RE.match(username):
32 raise forms.ValidationError(INVALID_NAME)
33 if username and username != self.instance.person_name:
34
35=== modified file 'src/identityprovider/tests/test_forms.py'
36--- src/identityprovider/tests/test_forms.py 2018-12-12 17:26:19 +0000
37+++ src/identityprovider/tests/test_forms.py 2019-04-03 17:04:50 +0000
38@@ -7,6 +7,7 @@
39 from django import forms
40 from django.conf import settings
41 from django.http import HttpRequest
42+from django.test import TestCase
43 from django.test.utils import override_settings
44 from django.utils.timezone import now
45 from django_openid_auth.teams import TeamsRequest
46@@ -35,6 +36,7 @@
47 )
48 from identityprovider.forms import (
49 INVALID_NAME,
50+ VALID_NAME_RE,
51 AccountStatusForm,
52 DeviceRenameForm,
53 EditAccountForm,
54@@ -58,6 +60,59 @@
55 from identityprovider.validators import BasicValidator
56
57
58+class ValidNameTestCase(TestCase):
59+
60+ def test_valid_names(self):
61+ names = [
62+ # 3 or more lowercase chars.
63+ 'foo',
64+ # Or chars and digits
65+ 'f00',
66+ # One or more dashes followed by at least one chars or digits.
67+ 'fo-o'
68+ 'f-oo',
69+ 'foo-1',
70+ '1-foo',
71+ 'f-o-o',
72+ # Up to 32 chars long
73+ 'a' * 32,
74+ ]
75+ for n in names:
76+ self.assertTrue(
77+ VALID_NAME_RE.match(n), '%r is unexpectedly invalid.' % n)
78+
79+ def test_invalid_names(self):
80+ names = [
81+ # 3 to 32 chars long.
82+ 'fu',
83+ 'a' * 33,
84+ # No uppercase-chars
85+ 'Foo',
86+ # No digits-only,
87+ '111',
88+ # No dashes-only.
89+ '---',
90+ # No single-char separator
91+ 'a-a',
92+ # No consecutive dashes.
93+ 'a--a',
94+ # No digit+hyphen-only.
95+ '1-23',
96+ # No DOTs.
97+ 'foo.bar',
98+ # No PLUSes.
99+ 'foo+bar',
100+ # No starting or ending with DASHes.
101+ '-foo',
102+ 'foo-',
103+ # No other special chars.
104+ 'foo%bar',
105+ ]
106+ for n in names:
107+ self.assertFalse(
108+ VALID_NAME_RE.match(n), '%r is unexpectedly valid.' % n)
109+
110+
111 class FormBaseTestCase(SSOBaseTestCase):
112
113 fields = []
114
115=== modified file 'src/identityprovider/tests/test_views_server.py'
116--- src/identityprovider/tests/test_views_server.py 2018-12-19 21:57:07 +0000
117+++ src/identityprovider/tests/test_views_server.py 2019-04-03 17:04:50 +0000
118@@ -1362,7 +1362,7 @@
119 data = {
120 'displayname': self.account.displayname,
121 'preferred_email': self.account.preferredemail.id,
122- 'username': 'test-valid+username.',
123+ 'username': 'test-valid-username',
124 'oldpassword': self.login_password,
125 }
126 response = self.client.post(self.url, data, follow=True)
127
128=== modified file 'src/webui/tests/test_views_account.py'
129--- src/webui/tests/test_views_account.py 2018-12-19 21:57:07 +0000
130+++ src/webui/tests/test_views_account.py 2019-04-03 17:04:50 +0000
131@@ -20,6 +20,7 @@
132 from pyquery import PyQuery
133
134 from identityprovider.const import SESSION_TOKEN_KEY, SESSION_TOKEN_NAME
135+from identityprovider.forms import INVALID_NAME
136 from identityprovider.models import (
137 Account,
138 AuthToken,
139@@ -402,12 +403,27 @@
140 r = self.post_username_change('BAD')
141 self.assertEqual(r.status_code, 200)
142 self.assertIsNone(self.account.person)
143- self.assertContains(
144- r,
145- 'Usernames must be at least two characters long and start with a '
146- 'letter or number. All letters must be lower-case. The '
147- 'characters +, - and . are also allowed after the first '
148- 'character.')
149+ self.assertContains(r, 'Usernames must be')
150+ self.assertContains(r, INVALID_NAME)
151+
152+ @switches(USERNAME_UI=True)
153+ def test_edit_username_preserves_bad_usernames(self):
154+ # Users with existing 'bad' usernames are not be forced to change them
155+ # just because we changed the criteria.
156+ bad_username = 'Bad+Taste.-'
157+ self.lp.set_lp_username(
158+ self.account.openid_identifier, bad_username)
159+ self.lp.flush_to_db()
160+ # 'bad' username is displayed as the default field value.
161+ r = self.client.get(self.url)
162+ self.assertContains(r, 'Username')
163+ self.assertContains(r, 'value="%s"' % bad_username)
164+ # The form can be submitted as is and will succeed.
165+ r = self.post_username_change(bad_username)
166+ self.assertEqual(r.status_code, 302)
167+ self.assertEqual(bad_username, self.account.person.name)
168+ r = self.client.get(self.url)
169+ self.assertContains(r, 'value="%s"' % bad_username)
170
171 def test_index_edit_password(self):
172 oauth_tokens = self.account.token_set.all()