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
=== modified file 'api/tests/test_handlers.py'
--- api/tests/test_handlers.py 2012-12-04 13:44:46 +0000
+++ api/tests/test_handlers.py 2012-12-13 20:21:20 +0000
@@ -27,6 +27,7 @@
27 EmailStatus,27 EmailStatus,
28 LoginTokenType,28 LoginTokenType,
29)29)
30from identityprovider.validators import PASSWORD_POLICY_ERROR
3031
31from api.tests.utils import (32from api.tests.utils import (
32 AnonAPITestCase,33 AnonAPITestCase,
@@ -51,8 +52,20 @@
51 response = self.api.registrations.set_new_password(52 response = self.api.registrations.set_new_password(
52 email='test@example.com', token=authtoken.token,53 email='test@example.com', token=authtoken.token,
53 new_password='pass')54 new_password='pass')
5455 self.assertEqual(response['status'], "error")
55 self.assertEqual(response['status'], "error")56
57 def test_set_new_password_invalid(self):
58 email_address = self.factory.make_email_address()
59 account = self.factory.make_account(email=email_address)
60 authtoken = AuthTokenFactory().new(account, email_address,
61 email_address,
62 LoginTokenType.PASSWORDRECOVERY,
63 None)
64
65 response = self.api.registrations.set_new_password(
66 email=email_address, token=authtoken.token, new_password='pass')
67 self.assertEqual(response['status'], "error")
68 self.assertEqual(response['errors'], [PASSWORD_POLICY_ERROR])
5669
57 def test_reset_token_when_email_is_invalid(self):70 def test_reset_token_when_email_is_invalid(self):
58 try:71 try:
5972
=== modified file 'api/v10/forms.py'
--- api/v10/forms.py 2012-12-03 16:51:35 +0000
+++ api/v10/forms.py 2012-12-13 20:21:20 +0000
@@ -7,18 +7,14 @@
7from django.forms import fields7from django.forms import fields
8from django.utils.translation import ugettext as _8from django.utils.translation import ugettext as _
99
10from identityprovider.utils import password_policy_compliant
11from identityprovider.models.captcha import Captcha10from identityprovider.models.captcha import Captcha
1211from identityprovider.validators import validate_password_policy
13
14PASSWORD_POLICY_ERROR = _("Password must be at least "
15 "8 characters long, and must contain at least one "
16 "number and an upper case letter.")
1712
1813
19class WebserviceCreateAccountForm(forms.Form):14class WebserviceCreateAccountForm(forms.Form):
20 email = fields.EmailField()15 email = fields.EmailField()
21 password = fields.CharField(max_length=256)16 password = fields.CharField(max_length=256,
17 validators=[validate_password_policy])
22 captcha_id = fields.CharField(max_length=1024)18 captcha_id = fields.CharField(max_length=1024)
23 captcha_solution = fields.CharField(max_length=256)19 captcha_solution = fields.CharField(max_length=256)
24 remote_ip = fields.CharField(max_length=256)20 remote_ip = fields.CharField(max_length=256)
@@ -28,18 +24,6 @@
28 empty_value='desktop', required=False)24 empty_value='desktop', required=False)
29 validate_redirect_to = fields.CharField(required=False)25 validate_redirect_to = fields.CharField(required=False)
3026
31 def clean_password(self):
32 if 'password' in self.cleaned_data:
33 password = self.cleaned_data['password']
34 try:
35 str(password)
36 except UnicodeEncodeError:
37 raise forms.ValidationError(
38 _("Invalid characters in password"))
39 if not password_policy_compliant(password):
40 raise forms.ValidationError(PASSWORD_POLICY_ERROR)
41 return password
42
43 def clean_validate_redirect_to(self):27 def clean_validate_redirect_to(self):
44 validate_redirect_to = self.cleaned_data.get('validate_redirect_to')28 validate_redirect_to = self.cleaned_data.get('validate_redirect_to')
45 # return None instead of '' as the default value29 # return None instead of '' as the default value
4630
=== modified file 'api/v10/handlers.py'
--- api/v10/handlers.py 2012-12-04 18:51:42 +0000
+++ api/v10/handlers.py 2012-12-13 20:21:20 +0000
@@ -4,6 +4,7 @@
4from datetime import datetime, timedelta4from datetime import datetime, timedelta
55
6from django.conf import settings6from django.conf import settings
7from django.core.exceptions import ValidationError
7from django.core.serializers.json import DateTimeAwareJSONEncoder8from django.core.serializers.json import DateTimeAwareJSONEncoder
8from django.http import (9from django.http import (
9 HttpResponseBadRequest,10 HttpResponseBadRequest,
@@ -36,10 +37,8 @@
36 application_token_created,37 application_token_created,
37 application_token_invalidated,38 application_token_invalidated,
38)39)
39from identityprovider.utils import (40from identityprovider.validators import validate_password_policy
40 encrypt_launchpad_password,41
41 password_policy_compliant,
42)
43from oauth_backend.models import Token42from oauth_backend.models import Token
4443
45from api.v10.decorators import (44from api.v10.decorators import (
@@ -48,7 +47,6 @@
48 named_operation,47 named_operation,
49)48)
50from api.v10.forms import (49from api.v10.forms import (
51 PASSWORD_POLICY_ERROR,
52 WebserviceCreateAccountForm,50 WebserviceCreateAccountForm,
53)51)
5452
@@ -199,15 +197,13 @@
199197
200 platform = cleaned_data['platform']198 platform = cleaned_data['platform']
201 redirection_url = cleaned_data['validate_redirect_to']199 redirection_url = cleaned_data['validate_redirect_to']
202 encrypted_password = encrypt_launchpad_password(200 password = cleaned_data['password']
203 cleaned_data['password'])
204 displayname = cleaned_data['displayname']201 displayname = cleaned_data['displayname']
205 email = cleaned_data['email']202 email = cleaned_data['email']
206 if platform in ['desktop', 'mobile']:203 if platform in ['desktop', 'mobile']:
207 account = Account.objects.create_account(204 account = Account.objects.create_account(
208 displayname, email,205 displayname, email, password, email_validated=False)
209 encrypted_password, password_encrypted=True,206 encrypted_password = account.encrypted_password
210 email_validated=False)
211207
212 if platform == 'desktop':208 if platform == 'desktop':
213 token = AuthTokenFactory().new_api_email_validation_token(209 token = AuthTokenFactory().new_api_email_validation_token(
@@ -275,15 +271,15 @@
275 'status': 'error',271 'status': 'error',
276 'message': "Wrong token, request new one."272 'message': "Wrong token, request new one."
277 }273 }
278 if not password_policy_compliant(new_password):274
275 try:
276 validate_password_policy(new_password)
277 except ValidationError, e:
279 return {278 return {
280 'status': 'error',279 'status': 'error',
281 'errors': [PASSWORD_POLICY_ERROR]280 'errors': e.messages,
282 }281 }
283 password_obj = token.requester.accountpassword282 token.requester.set_password(new_password)
284 password_obj.password = encrypt_launchpad_password(new_password)
285 password_obj.save()
286
287 token.consume()283 token.consume()
288284
289 return {285 return {
290286
=== modified file 'identityprovider/forms.py'
--- identityprovider/forms.py 2012-10-30 12:15:29 +0000
+++ identityprovider/forms.py 2012-12-13 20:21:20 +0000
@@ -21,7 +21,9 @@
21from identityprovider.models.const import EmailStatus21from identityprovider.models.const import EmailStatus
22from identityprovider.utils import (22from identityprovider.utils import (
23 encrypt_launchpad_password,23 encrypt_launchpad_password,
24 password_policy_compliant,24)
25from identityprovider.validators import (
26 validate_password_policy,
25)27)
26from identityprovider.fields import OATHPasswordField28from identityprovider.fields import OATHPasswordField
27from identityprovider.widgets import (29from identityprovider.widgets import (
@@ -29,8 +31,6 @@
29 ROAwareSelect,31 ROAwareSelect,
30)32)
3133
32PASSWORD_ERROR = _("Password must be at least 8 characters long, and must "
33 "contain at least one number and an upper case letter.")
3434
35logger = logging.getLogger('sso')35logger = logging.getLogger('sso')
3636
@@ -81,6 +81,7 @@
81class ResetPasswordForm(forms.Form):81class ResetPasswordForm(forms.Form):
82 password = fields.CharField(82 password = fields.CharField(
83 error_messages=default_errors,83 error_messages=default_errors,
84 validators=[validate_password_policy],
84 widget=widgets.PasswordInput(attrs={85 widget=widgets.PasswordInput(attrs={
85 'class': 'textType',86 'class': 'textType',
86 'size': '20',87 'size': '20',
@@ -94,18 +95,6 @@
94 }),95 }),
95 )96 )
9697
97 def clean_password(self):
98 if 'password' in self.cleaned_data:
99 password = self.cleaned_data['password']
100 try:
101 str(password)
102 except UnicodeEncodeError:
103 raise forms.ValidationError(
104 _("Invalid characters in password"))
105 if not password_policy_compliant(password):
106 raise forms.ValidationError(PASSWORD_ERROR)
107 return password
108
109 def clean(self):98 def clean(self):
110 cleaned_data = self.cleaned_data99 cleaned_data = self.cleaned_data
111 password = cleaned_data.get('password')100 password = cleaned_data.get('password')
@@ -171,6 +160,7 @@
171 )160 )
172 password = fields.CharField(161 password = fields.CharField(
173 required=False,162 required=False,
163 validators=[validate_password_policy],
174 widget=widgets.PasswordInput(attrs={164 widget=widgets.PasswordInput(attrs={
175 'class': 'disableAutoComplete textType',165 'class': 'disableAutoComplete textType',
176 'size': '20',166 'size': '20',
@@ -237,14 +227,6 @@
237 (self.data.get('currentpassword') or227 (self.data.get('currentpassword') or
238 self.data.get('passwordconfirm'))):228 self.data.get('passwordconfirm'))):
239 raise forms.ValidationError(_("Required field"))229 raise forms.ValidationError(_("Required field"))
240 if password:
241 try:
242 str(password)
243 except UnicodeEncodeError:
244 raise forms.ValidationError(
245 _("Invalid characters in password"))
246 if not password_policy_compliant(password):
247 raise forms.ValidationError(PASSWORD_ERROR)
248 return password230 return password
249231
250 def clean_passwordconfirm(self):232 def clean_passwordconfirm(self):
251233
=== modified file 'identityprovider/models/account.py'
--- identityprovider/models/account.py 2012-10-29 12:47:16 +0000
+++ identityprovider/models/account.py 2012-12-13 20:21:20 +0000
@@ -278,6 +278,24 @@
278 sname = self.displayname.split(' ')278 sname = self.displayname.split(' ')
279 return sname[0]279 return sname[0]
280280
281 @property
282 def encrypted_password(self):
283 try:
284 accountpassword = self.accountpassword
285 password = accountpassword.password
286 except AccountPassword.DoesNotExist:
287 password = None
288 return password
289
290 def set_password(self, password, salt=None):
291 try:
292 accountpassword = self.accountpassword
293 except AccountPassword.DoesNotExist:
294 accountpassword = AccountPassword(account=self, password='invalid')
295 accountpassword.password = encrypt_launchpad_password(
296 password, salt=salt)
297 accountpassword.save()
298
281 def get_and_delete_messages(self):299 def get_and_delete_messages(self):
282 return []300 return []
283301
284302
=== modified file 'identityprovider/models/emailaddress.py'
--- identityprovider/models/emailaddress.py 2012-11-29 14:47:09 +0000
+++ identityprovider/models/emailaddress.py 2012-12-13 20:21:20 +0000
@@ -7,6 +7,7 @@
7from django.conf import settings7from django.conf import settings
8from django.core.mail import send_mail8from django.core.mail import send_mail
9from django.core.urlresolvers import reverse9from django.core.urlresolvers import reverse
10from django.core.validators import validate_email
10from django.db import models11from django.db import models
11from django.template.defaultfilters import force_escape12from django.template.defaultfilters import force_escape
12from django.template.loader import render_to_string13from django.template.loader import render_to_string
@@ -22,7 +23,7 @@
2223
2324
24class EmailAddress(models.Model):25class EmailAddress(models.Model):
25 email = models.TextField()26 email = models.TextField(validators=[validate_email])
26 lp_person = models.ForeignKey(27 lp_person = models.ForeignKey(
27 Person, db_column='person', blank=True, null=True, editable=False)28 Person, db_column='person', blank=True, null=True, editable=False)
28 status = models.IntegerField(choices=EmailStatus._get_choices())29 status = models.IntegerField(choices=EmailStatus._get_choices())
2930
=== added file 'identityprovider/tests/unit/test_validators.py'
--- identityprovider/tests/unit/test_validators.py 1970-01-01 00:00:00 +0000
+++ identityprovider/tests/unit/test_validators.py 2012-12-13 20:21:20 +0000
@@ -0,0 +1,26 @@
1from django.core.exceptions import ValidationError
2
3from identityprovider.tests.utils import SSOBaseTestCase
4from identityprovider.validators import validate_password_policy
5
6
7class PasswordPolicyValidatorTestCase(SSOBaseTestCase):
8
9 def test_password_too_short(self):
10 self.assertRaises(
11 ValidationError, validate_password_policy, 'abcd3Fg')
12
13 def test_password_missing_uppercase(self):
14 self.assertRaises(
15 ValidationError, validate_password_policy, 'abcd3fgh')
16
17 def test_password_missing_number(self):
18 self.assertRaises(
19 ValidationError, validate_password_policy, 'abcdEfgh')
20
21 def test_password_invalid_chars(self):
22 self.assertRaises(
23 ValidationError, validate_password_policy, u'abcd\xe1Fgh')
24
25 def test_valid_password(self):
26 self.assertEqual(validate_password_policy('abcD3fgh'), None)
027
=== added file 'identityprovider/validators.py'
--- identityprovider/validators.py 1970-01-01 00:00:00 +0000
+++ identityprovider/validators.py 2012-12-13 20:21:20 +0000
@@ -0,0 +1,21 @@
1from django.core.exceptions import ValidationError
2from django.utils.translation import ugettext_lazy as _
3
4from identityprovider.utils import (
5 password_policy_compliant,
6)
7
8
9PASSWORD_POLICY_ERROR = _("Password must be at least "
10 "8 characters long, and must contain at least one "
11 "number and an upper case letter.")
12
13
14def validate_password_policy(password):
15 """Validate password complies with policy."""
16 try:
17 str(password)
18 except UnicodeEncodeError:
19 raise ValidationError(_("Invalid characters in password"))
20 if not password_policy_compliant(password):
21 raise ValidationError(PASSWORD_POLICY_ERROR)
022
=== modified file 'webui/views/ui.py'
--- webui/views/ui.py 2012-12-06 14:31:23 +0000
+++ webui/views/ui.py 2012-12-13 20:21:20 +0000
@@ -48,7 +48,6 @@
48)48)
49from identityprovider.models import (49from identityprovider.models import (
50 Account,50 Account,
51 AccountPassword,
52 AuthToken,51 AuthToken,
53 AuthTokenFactory,52 AuthTokenFactory,
54 EmailAddress,53 EmailAddress,
@@ -1023,15 +1022,7 @@
1023 if not created:1022 if not created:
1024 account.preferredemail = email_obj1023 account.preferredemail = email_obj
1025 email = account.preferredemail.email1024 email = account.preferredemail.email
1026 try:1025 account.set_password(password)
1027 password_obj = account.accountpassword
1028 except AccountPassword.DoesNotExist:
1029 password_obj = AccountPassword.objects.create(
1030 account=account,
1031 password='invalid',
1032 )
1033 password_obj.password = encrypt_launchpad_password(password)
1034 password_obj.save()
1035 user = auth.authenticate(username=email, password=password)1026 user = auth.authenticate(username=email, password=password)
1036 auth.login(request, user)1027 auth.login(request, user)
1037 atrequest.consume()1028 atrequest.consume()