Merge lp:~james-w/canonical-identity-provider/email-timeout into lp:canonical-identity-provider/release

Proposed by James Westby
Status: Merged
Approved by: James Westby
Approved revision: no longer in the source branch.
Merged at revision: 1022
Proposed branch: lp:~james-w/canonical-identity-provider/email-timeout
Merge into: lp:canonical-identity-provider/release
Diff against target: 69 lines (+43/-1)
2 files modified
src/identityprovider/emailutils.py (+1/-1)
src/identityprovider/tests/test_emailutils.py (+42/-0)
To merge this branch: bzr merge lp:~james-w/canonical-identity-provider/email-timeout
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+181650@code.launchpad.net

Commit message

Treat email as case-insensitive when checking for verified emails.

The db indexes are for UPPER/LOWER on email, so not using __iexact
means that no index is used. In most other places email is taken
to be case-insensitive.

Description of the change

Hi,

As explained in the commit message use __iexact on an email lookup.

https://oops.canonical.com/oops/?oopsid=OOPS-ac9482fbb99b4e580c1434073ad0aa93 for an example of
the problem this fixes.

Thanks,

James

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

Updated with tests.

I feel a bit uneasy about this change though. It seems like the inconsistent
treatment of email addresses means that there could be bugs hiding, including
possibly security holes.

Here's what I've found:

management.commands.add_to_team: case-sensitive lookup: mostly fine, just have to lookup with the correct case
management.commands.create_test_team: case-sensitive lookup: would create a new email address with the settings.SSO_TEST_ACCOUNT_EMAIL case.
management.commands.cleanup: case-insensitive deletion
forms: case-insensitive lookup, both for previously invalidated, and duplicate check. However, duplicate check only activates if status=NEW. I don't see where the email is actually created based on this though.
login: case-insensitive lookup against invalidated,
models.emailaddress: create_from_phone_id: no case insensitive check before creation.
views.ui: _finish_account_creation: get_by_email, then case-sensitive lookup. Would just cause a crash if mismatch.
          confirm-email: case-insensitive lookup
          reset_password: case-insensitive lookup
views.account: _send_verification_email: case-insensitive delete, followed by case-sensitive lookup
               invalidate_email: case-sensitive lookup
api.v10.handlers: RegistrationHandler: case-insensitive lookup
                  validate_email: case-insensitive lookup for validation
api.v20.handlers: EmailsHandler: case-sensitive lookup/deletion

There's nothing obviously terrible here, but there is clearly some confusion. I wonder if it
is worth clearing this up? Or whether the differences are warranted?

Thanks,

James

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

LGTM. Thanks for the tests.

review: Approve
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

> Updated with tests.
>
> I feel a bit uneasy about this change though. It seems like the inconsistent
> treatment of email addresses means that there could be bugs hiding, including
> possibly security holes.
>
> Here's what I've found:
>
> management.commands.add_to_team: case-sensitive lookup: mostly fine, just have
> to lookup with the correct case
> management.commands.create_test_team: case-sensitive lookup: would create a
> new email address with the settings.SSO_TEST_ACCOUNT_EMAIL case.
> management.commands.cleanup: case-insensitive deletion
> forms: case-insensitive lookup, both for previously invalidated, and duplicate
> check. However, duplicate check only activates if status=NEW. I don't see
> where the email is actually created based on this though.
> login: case-insensitive lookup against invalidated,
> models.emailaddress: create_from_phone_id: no case insensitive check before
> creation.
> views.ui: _finish_account_creation: get_by_email, then case-sensitive lookup.
> Would just cause a crash if mismatch.
> confirm-email: case-insensitive lookup
> reset_password: case-insensitive lookup
> views.account: _send_verification_email: case-insensitive delete, followed by
> case-sensitive lookup
> invalidate_email: case-sensitive lookup
> api.v10.handlers: RegistrationHandler: case-insensitive lookup
> validate_email: case-insensitive lookup for validation
> api.v20.handlers: EmailsHandler: case-sensitive lookup/deletion
>
>
> There's nothing obviously terrible here, but there is clearly some confusion.
> I wonder if it
> is worth clearing this up? Or whether the differences are warranted?
>

It's probably worth to work on these issues at some point, at the very least make sure we always use the case insensitive approach.

> Thanks,
>
> James

Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (258.2 KiB)

The attempt to merge lp:~james-w/canonical-identity-provider/email-timeout into lp:canonical-identity-provider failed. Below is the output from the failed tests.

Branching the download cache in dir /mnt/tarmac/cache/canonical-identity-provider/isd-download-cache
[localhost] local: which virtualenv
[localhost] local: /usr/bin/python /usr/bin/virtualenv --distribute --clear --system-site-packages .env
Not deleting .env/bin
New python executable in .env/bin/python
Installing distribute.............................................................................................................................................................................................done.
Installing pip...............done.
[localhost] local: dpkg -l libpq-dev 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l libxml2-dev 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l libxslt1-dev 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l memcached 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l postgresql-plpython-9.1 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l python-dev 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l python-m2crypto 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l swig 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l config-manager 2> /dev/null | grep '^ii' | wc -l
[localhost] local: dpkg -l python-egenix-mx-base-dev 2> /dev/null | grep '^ii' | wc -l
[localhost] local: /usr/lib/config-manager/cm.py update /tmp/tmp5CcF99
[localhost] local: . /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/.env/bin/activate && make install PACKAGES="-r /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/requirements.txt"
pip install --find-links=file:///mnt/tarmac/cache/canonical-identity-provider/isd-download-cache --no-index pip==dev
Ignoring indexes: http://pypi.python.org/simple/
Downloading/unpacking pip==dev
  Running setup.py egg_info for package pip

    warning: no files found matching '*.html' under directory 'docs'
    warning: no previously-included files matching '*.txt' found under directory 'docs/_build'
    no previously-included directories found matching 'docs/_build/_sources'
Installing collected packages: pip
  Found existing installation: pip 1.1
    Uninstalling pip:
      Successfully uninstalled pip
  Running setup.py install for pip

    warning: no files found matching '*.html' under directory 'docs'
    warning: no previously-included files matching '*.txt' found under directory 'docs/_build'
    no previously-included directories found matching 'docs/_build/_sources'
    Installing pip script to /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/.env/bin
    Installing pip-2.7 script to /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/.env/bin
Successfully installed pip
Cleaning up...
pip install --find-links=. --no-index -r /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/requirements.txt
Ignoring indexes: http://pypi.python.org/simple/
Downloading/unpacking bzr+http://bazaar.launchpad.net/~canonical-isd-hackers/u1-test-utils/trunk@81 (from -r /mnt/tarmac/cache/canonical-identity-prov...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/identityprovider/emailutils.py'
2--- src/identityprovider/emailutils.py 2013-08-12 14:33:47 +0000
3+++ src/identityprovider/emailutils.py 2013-08-23 13:39:28 +0000
4@@ -95,7 +95,7 @@
5 def _should_add_invalidation_link(email):
6 account = Account.objects.get_by_email(email)
7 if gargoyle.is_active('ALLOW_UNVERIFIED', account):
8- emails = EmailAddress.objects.filter(email=email)
9+ emails = EmailAddress.objects.filter(email__iexact=email)
10 result = (emails.count() == 0 or
11 emails.filter(status=EmailStatus.NEW).count() > 0)
12 return result
13
14=== modified file 'src/identityprovider/tests/test_emailutils.py'
15--- src/identityprovider/tests/test_emailutils.py 2013-08-12 16:32:49 +0000
16+++ src/identityprovider/tests/test_emailutils.py 2013-08-23 13:39:28 +0000
17@@ -24,6 +24,7 @@
18 send_templated_email,
19 send_twofactor_failure_warning,
20 send_validation_email_request,
21+ _should_add_invalidation_link,
22 )
23 from identityprovider.models import Account, AuthToken, EmailAddress
24 from identityprovider.models.const import EmailStatus, TokenType
25@@ -700,3 +701,44 @@
26 def test_recipients(self):
27 email = self.send_email()
28 self.assertEqual(self.recipients, email.recipients())
29+
30+
31+class ShouldAddInvalidationLinkTests(SSOBaseTestCase):
32+
33+ @switches(ALLOW_UNVERIFIED=False)
34+ def test_disallow_unverified_means_false(self):
35+ email = self.factory.make_email_address()
36+ self.assertEqual(False, _should_add_invalidation_link(email))
37+
38+ @switches(ALLOW_UNVERIFIED=True)
39+ def test_an_email_means_false(self):
40+ email = self.factory.make_email_address()
41+ account = self.factory.make_account()
42+ self.factory.make_email_for_account(account, email=email,
43+ status=EmailStatus.VALIDATED)
44+ self.assertEqual(False,
45+ _should_add_invalidation_link(email))
46+
47+ @switches(ALLOW_UNVERIFIED=True)
48+ def test_an_email_means_false_different_case(self):
49+ email = self.factory.make_email_address()
50+ account = self.factory.make_account()
51+ self.factory.make_email_for_account(account, email=email,
52+ status=EmailStatus.VALIDATED)
53+ self.assertEqual(False,
54+ _should_add_invalidation_link(email.upper()))
55+
56+ @switches(ALLOW_UNVERIFIED=True)
57+ def test_no_emails_means_true(self):
58+ email = self.factory.make_email_address()
59+ self.assertEqual(True,
60+ _should_add_invalidation_link(email))
61+
62+ @switches(ALLOW_UNVERIFIED=True)
63+ def test_new_emails_means_true(self):
64+ email = self.factory.make_email_address()
65+ account = self.factory.make_account()
66+ self.factory.make_email_for_account(account, email=email,
67+ status=EmailStatus.NEW)
68+ self.assertEqual(True,
69+ _should_add_invalidation_link(email))