Merge lp:~matiasb/canonical-identity-provider/models-validation into lp:canonical-identity-provider/release

Proposed by Matias Bordese
Status: Merged
Approved by: Natalia Bidart
Approved revision: no longer in the source branch.
Merged at revision: 556
Proposed branch: lp:~matiasb/canonical-identity-provider/models-validation
Merge into: lp:canonical-identity-provider/release
Diff against target: 363 lines (+103/-71)
9 files modified
api/tests/test_handlers.py (+15/-2)
api/v10/forms.py (+3/-19)
api/v10/handlers.py (+12/-16)
identityprovider/forms.py (+5/-23)
identityprovider/models/account.py (+18/-0)
identityprovider/models/emailaddress.py (+2/-1)
identityprovider/tests/unit/test_validators.py (+26/-0)
identityprovider/validators.py (+21/-0)
webui/views/ui.py (+1/-10)
To merge this branch: bzr merge lp:~matiasb/canonical-identity-provider/models-validation
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Review via email: mp+139547@code.launchpad.net

Commit message

Refactored password and email validation when creating an account using (reusable) validators.

Description of the change

Refactored password and email validation when creating an account using (reusable) validators.
Updated forms/views requiring password validation.
Kept flexibility at the model level (by using set_password or create_account).

To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Code looks good. This piece seems buggy to me, perhaps you can fix it and add a test for it as well?

108 - 'errors': [PASSWORD_POLICY_ERROR]
109 + 'errors': e.message

Also, could you please add a couple of more tests to PasswordPolicyValidatorTestCase?

review: Needs Fixing
Revision history for this message
Matias Bordese (matiasb) wrote :

> Code looks good. This piece seems buggy to me, perhaps you can fix it and add
> a test for it as well?
>
> 108 - 'errors': [PASSWORD_POLICY_ERROR]
> 109 + 'errors': e.message

Added test, using .messages returns a list (of possible multiple errors).

> Also, could you please add a couple of more tests to
> PasswordPolicyValidatorTestCase?

Added.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks good, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'api/tests/test_handlers.py'
2--- api/tests/test_handlers.py 2012-12-04 13:44:46 +0000
3+++ api/tests/test_handlers.py 2012-12-13 20:21:20 +0000
4@@ -27,6 +27,7 @@
5 EmailStatus,
6 LoginTokenType,
7 )
8+from identityprovider.validators import PASSWORD_POLICY_ERROR
9
10 from api.tests.utils import (
11 AnonAPITestCase,
12@@ -51,8 +52,20 @@
13 response = self.api.registrations.set_new_password(
14 email='test@example.com', token=authtoken.token,
15 new_password='pass')
16-
17- self.assertEqual(response['status'], "error")
18+ self.assertEqual(response['status'], "error")
19+
20+ def test_set_new_password_invalid(self):
21+ email_address = self.factory.make_email_address()
22+ account = self.factory.make_account(email=email_address)
23+ authtoken = AuthTokenFactory().new(account, email_address,
24+ email_address,
25+ LoginTokenType.PASSWORDRECOVERY,
26+ None)
27+
28+ response = self.api.registrations.set_new_password(
29+ email=email_address, token=authtoken.token, new_password='pass')
30+ self.assertEqual(response['status'], "error")
31+ self.assertEqual(response['errors'], [PASSWORD_POLICY_ERROR])
32
33 def test_reset_token_when_email_is_invalid(self):
34 try:
35
36=== modified file 'api/v10/forms.py'
37--- api/v10/forms.py 2012-12-03 16:51:35 +0000
38+++ api/v10/forms.py 2012-12-13 20:21:20 +0000
39@@ -7,18 +7,14 @@
40 from django.forms import fields
41 from django.utils.translation import ugettext as _
42
43-from identityprovider.utils import password_policy_compliant
44 from identityprovider.models.captcha import Captcha
45-
46-
47-PASSWORD_POLICY_ERROR = _("Password must be at least "
48- "8 characters long, and must contain at least one "
49- "number and an upper case letter.")
50+from identityprovider.validators import validate_password_policy
51
52
53 class WebserviceCreateAccountForm(forms.Form):
54 email = fields.EmailField()
55- password = fields.CharField(max_length=256)
56+ password = fields.CharField(max_length=256,
57+ validators=[validate_password_policy])
58 captcha_id = fields.CharField(max_length=1024)
59 captcha_solution = fields.CharField(max_length=256)
60 remote_ip = fields.CharField(max_length=256)
61@@ -28,18 +24,6 @@
62 empty_value='desktop', required=False)
63 validate_redirect_to = fields.CharField(required=False)
64
65- def clean_password(self):
66- if 'password' in self.cleaned_data:
67- password = self.cleaned_data['password']
68- try:
69- str(password)
70- except UnicodeEncodeError:
71- raise forms.ValidationError(
72- _("Invalid characters in password"))
73- if not password_policy_compliant(password):
74- raise forms.ValidationError(PASSWORD_POLICY_ERROR)
75- return password
76-
77 def clean_validate_redirect_to(self):
78 validate_redirect_to = self.cleaned_data.get('validate_redirect_to')
79 # return None instead of '' as the default value
80
81=== modified file 'api/v10/handlers.py'
82--- api/v10/handlers.py 2012-12-04 18:51:42 +0000
83+++ api/v10/handlers.py 2012-12-13 20:21:20 +0000
84@@ -4,6 +4,7 @@
85 from datetime import datetime, timedelta
86
87 from django.conf import settings
88+from django.core.exceptions import ValidationError
89 from django.core.serializers.json import DateTimeAwareJSONEncoder
90 from django.http import (
91 HttpResponseBadRequest,
92@@ -36,10 +37,8 @@
93 application_token_created,
94 application_token_invalidated,
95 )
96-from identityprovider.utils import (
97- encrypt_launchpad_password,
98- password_policy_compliant,
99-)
100+from identityprovider.validators import validate_password_policy
101+
102 from oauth_backend.models import Token
103
104 from api.v10.decorators import (
105@@ -48,7 +47,6 @@
106 named_operation,
107 )
108 from api.v10.forms import (
109- PASSWORD_POLICY_ERROR,
110 WebserviceCreateAccountForm,
111 )
112
113@@ -199,15 +197,13 @@
114
115 platform = cleaned_data['platform']
116 redirection_url = cleaned_data['validate_redirect_to']
117- encrypted_password = encrypt_launchpad_password(
118- cleaned_data['password'])
119+ password = cleaned_data['password']
120 displayname = cleaned_data['displayname']
121 email = cleaned_data['email']
122 if platform in ['desktop', 'mobile']:
123 account = Account.objects.create_account(
124- displayname, email,
125- encrypted_password, password_encrypted=True,
126- email_validated=False)
127+ displayname, email, password, email_validated=False)
128+ encrypted_password = account.encrypted_password
129
130 if platform == 'desktop':
131 token = AuthTokenFactory().new_api_email_validation_token(
132@@ -275,15 +271,15 @@
133 'status': 'error',
134 'message': "Wrong token, request new one."
135 }
136- if not password_policy_compliant(new_password):
137+
138+ try:
139+ validate_password_policy(new_password)
140+ except ValidationError, e:
141 return {
142 'status': 'error',
143- 'errors': [PASSWORD_POLICY_ERROR]
144+ 'errors': e.messages,
145 }
146- password_obj = token.requester.accountpassword
147- password_obj.password = encrypt_launchpad_password(new_password)
148- password_obj.save()
149-
150+ token.requester.set_password(new_password)
151 token.consume()
152
153 return {
154
155=== modified file 'identityprovider/forms.py'
156--- identityprovider/forms.py 2012-10-30 12:15:29 +0000
157+++ identityprovider/forms.py 2012-12-13 20:21:20 +0000
158@@ -21,7 +21,9 @@
159 from identityprovider.models.const import EmailStatus
160 from identityprovider.utils import (
161 encrypt_launchpad_password,
162- password_policy_compliant,
163+)
164+from identityprovider.validators import (
165+ validate_password_policy,
166 )
167 from identityprovider.fields import OATHPasswordField
168 from identityprovider.widgets import (
169@@ -29,8 +31,6 @@
170 ROAwareSelect,
171 )
172
173-PASSWORD_ERROR = _("Password must be at least 8 characters long, and must "
174- "contain at least one number and an upper case letter.")
175
176 logger = logging.getLogger('sso')
177
178@@ -81,6 +81,7 @@
179 class ResetPasswordForm(forms.Form):
180 password = fields.CharField(
181 error_messages=default_errors,
182+ validators=[validate_password_policy],
183 widget=widgets.PasswordInput(attrs={
184 'class': 'textType',
185 'size': '20',
186@@ -94,18 +95,6 @@
187 }),
188 )
189
190- def clean_password(self):
191- if 'password' in self.cleaned_data:
192- password = self.cleaned_data['password']
193- try:
194- str(password)
195- except UnicodeEncodeError:
196- raise forms.ValidationError(
197- _("Invalid characters in password"))
198- if not password_policy_compliant(password):
199- raise forms.ValidationError(PASSWORD_ERROR)
200- return password
201-
202 def clean(self):
203 cleaned_data = self.cleaned_data
204 password = cleaned_data.get('password')
205@@ -171,6 +160,7 @@
206 )
207 password = fields.CharField(
208 required=False,
209+ validators=[validate_password_policy],
210 widget=widgets.PasswordInput(attrs={
211 'class': 'disableAutoComplete textType',
212 'size': '20',
213@@ -237,14 +227,6 @@
214 (self.data.get('currentpassword') or
215 self.data.get('passwordconfirm'))):
216 raise forms.ValidationError(_("Required field"))
217- if password:
218- try:
219- str(password)
220- except UnicodeEncodeError:
221- raise forms.ValidationError(
222- _("Invalid characters in password"))
223- if not password_policy_compliant(password):
224- raise forms.ValidationError(PASSWORD_ERROR)
225 return password
226
227 def clean_passwordconfirm(self):
228
229=== modified file 'identityprovider/models/account.py'
230--- identityprovider/models/account.py 2012-10-29 12:47:16 +0000
231+++ identityprovider/models/account.py 2012-12-13 20:21:20 +0000
232@@ -278,6 +278,24 @@
233 sname = self.displayname.split(' ')
234 return sname[0]
235
236+ @property
237+ def encrypted_password(self):
238+ try:
239+ accountpassword = self.accountpassword
240+ password = accountpassword.password
241+ except AccountPassword.DoesNotExist:
242+ password = None
243+ return password
244+
245+ def set_password(self, password, salt=None):
246+ try:
247+ accountpassword = self.accountpassword
248+ except AccountPassword.DoesNotExist:
249+ accountpassword = AccountPassword(account=self, password='invalid')
250+ accountpassword.password = encrypt_launchpad_password(
251+ password, salt=salt)
252+ accountpassword.save()
253+
254 def get_and_delete_messages(self):
255 return []
256
257
258=== modified file 'identityprovider/models/emailaddress.py'
259--- identityprovider/models/emailaddress.py 2012-11-29 14:47:09 +0000
260+++ identityprovider/models/emailaddress.py 2012-12-13 20:21:20 +0000
261@@ -7,6 +7,7 @@
262 from django.conf import settings
263 from django.core.mail import send_mail
264 from django.core.urlresolvers import reverse
265+from django.core.validators import validate_email
266 from django.db import models
267 from django.template.defaultfilters import force_escape
268 from django.template.loader import render_to_string
269@@ -22,7 +23,7 @@
270
271
272 class EmailAddress(models.Model):
273- email = models.TextField()
274+ email = models.TextField(validators=[validate_email])
275 lp_person = models.ForeignKey(
276 Person, db_column='person', blank=True, null=True, editable=False)
277 status = models.IntegerField(choices=EmailStatus._get_choices())
278
279=== added file 'identityprovider/tests/unit/test_validators.py'
280--- identityprovider/tests/unit/test_validators.py 1970-01-01 00:00:00 +0000
281+++ identityprovider/tests/unit/test_validators.py 2012-12-13 20:21:20 +0000
282@@ -0,0 +1,26 @@
283+from django.core.exceptions import ValidationError
284+
285+from identityprovider.tests.utils import SSOBaseTestCase
286+from identityprovider.validators import validate_password_policy
287+
288+
289+class PasswordPolicyValidatorTestCase(SSOBaseTestCase):
290+
291+ def test_password_too_short(self):
292+ self.assertRaises(
293+ ValidationError, validate_password_policy, 'abcd3Fg')
294+
295+ def test_password_missing_uppercase(self):
296+ self.assertRaises(
297+ ValidationError, validate_password_policy, 'abcd3fgh')
298+
299+ def test_password_missing_number(self):
300+ self.assertRaises(
301+ ValidationError, validate_password_policy, 'abcdEfgh')
302+
303+ def test_password_invalid_chars(self):
304+ self.assertRaises(
305+ ValidationError, validate_password_policy, u'abcd\xe1Fgh')
306+
307+ def test_valid_password(self):
308+ self.assertEqual(validate_password_policy('abcD3fgh'), None)
309
310=== added file 'identityprovider/validators.py'
311--- identityprovider/validators.py 1970-01-01 00:00:00 +0000
312+++ identityprovider/validators.py 2012-12-13 20:21:20 +0000
313@@ -0,0 +1,21 @@
314+from django.core.exceptions import ValidationError
315+from django.utils.translation import ugettext_lazy as _
316+
317+from identityprovider.utils import (
318+ password_policy_compliant,
319+)
320+
321+
322+PASSWORD_POLICY_ERROR = _("Password must be at least "
323+ "8 characters long, and must contain at least one "
324+ "number and an upper case letter.")
325+
326+
327+def validate_password_policy(password):
328+ """Validate password complies with policy."""
329+ try:
330+ str(password)
331+ except UnicodeEncodeError:
332+ raise ValidationError(_("Invalid characters in password"))
333+ if not password_policy_compliant(password):
334+ raise ValidationError(PASSWORD_POLICY_ERROR)
335
336=== modified file 'webui/views/ui.py'
337--- webui/views/ui.py 2012-12-06 14:31:23 +0000
338+++ webui/views/ui.py 2012-12-13 20:21:20 +0000
339@@ -48,7 +48,6 @@
340 )
341 from identityprovider.models import (
342 Account,
343- AccountPassword,
344 AuthToken,
345 AuthTokenFactory,
346 EmailAddress,
347@@ -1023,15 +1022,7 @@
348 if not created:
349 account.preferredemail = email_obj
350 email = account.preferredemail.email
351- try:
352- password_obj = account.accountpassword
353- except AccountPassword.DoesNotExist:
354- password_obj = AccountPassword.objects.create(
355- account=account,
356- password='invalid',
357- )
358- password_obj.password = encrypt_launchpad_password(password)
359- password_obj.save()
360+ account.set_password(password)
361 user = auth.authenticate(username=email, password=password)
362 auth.login(request, user)
363 atrequest.consume()