← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jcsackett/launchpad/dont-expose-domains into lp:launchpad

 

j.c.sackett has proposed merging lp:~jcsackett/launchpad/dont-expose-domains into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #228355 in Launchpad itself: "Unique name reveals hidden email address"
  https://bugs.launchpad.net/launchpad/+bug/228355

For more details, see:
https://code.launchpad.net/~jcsackett/launchpad/dont-expose-domains/+merge/62023

Summary
=======
Even though users can set lp to not show their email address, if their id has been automatically generated the email can still essentially be divulged b/c it's used to create the idea. (e.g. bob@xxxxxxxxx can become bob-gmail if bob is already taken or bob-gmail-com).

This code removes the domain bit from the nick generation, as there is already a random suffix generation piece as a final fall back. If bob is already taken as a username, than the next bob@somedomain becomes bob-c or similar.

Preimplementation
=================
Spoke with Curtis Hovey

Implementation
==============
lib/lp/registry/model/person.py
-------------------------------
Removed the portion of generate_nick that used the email domain to create nicks.

lib/lp/registry/tests/test_nickname.py
lib/lp/registry/doc/nickname.txt
--------------------------------
Removed the doc tests and replaced it with unittests, which are more appropriate.

Tests
=====
bin/test -t test_nickname

QA
==
Create a user on qastaging with a an email that could collide with an existing user (e.g. jcsackett@xxxxxxxxxxx). The domain should not be part of the generated nick.

Lint
====

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_nickname.py

-- 
https://code.launchpad.net/~jcsackett/launchpad/dont-expose-domains/+merge/62023
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jcsackett/launchpad/dont-expose-domains into lp:launchpad.
=== removed file 'lib/lp/registry/doc/nickname.txt'
--- lib/lp/registry/doc/nickname.txt	2010-10-09 16:36:22 +0000
+++ lib/lp/registry/doc/nickname.txt	1970-01-01 00:00:00 +0000
@@ -1,96 +0,0 @@
-= Nickname =
-
-For each Person we create a unique nickname that must be generated using
-generate_nick.
-
-A valid nick can contain lower case letters, dashes, and numbers,
-must start with a letter or a number, and must be a minimum of
-four characters.
-
-    >>> from lp.registry.model.person import generate_nick
-    >>> generate_nick("foop@example@com")
-    Traceback (most recent call last):
-        ...
-    NicknameGenerationError: foop@example@com is not a valid email address
-    >>> generate_nick("foop@xxxxxxxxxxx")
-    'foop'
-    >>> generate_nick("bar@xxxxxxxxxxx")
-    'bar'
-    >>> generate_nick("spam@xxxxxxxxxxx")
-    'spam'
-    >>> generate_nick("foo.bar@xxxxxxxxxxxxx")
-    'foo-bar'
-    >>> generate_nick("foo+bar@xxxxxxxxxxx")
-    'foo+bar'
-    >>> generate_nick("--foo@xxxxxxxxxxx")
-    'foo'
-
-We already have a salgado, so generate_nick needs to try a little harder.
-
-    >>> generate_nick("salgado@xxxxxxxxxxx")
-    'salgado-async'
-
-And it needs to maintain a minimum length
-
-    >>> generate_nick("i@tv")
-    'i-tv'
-
-    >>> _counter = 0
-    >>> from zope.security.proxy import removeSecurityProxy
-    >>> def new_person(email):
-    ...     from lp.registry.interfaces.person import IPersonSet
-    ...     from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
-    ...     from lp.registry.interfaces.person import (
-    ...         PersonCreationRationale)
-    ...     UNKNOWN = PersonCreationRationale.UNKNOWN
-    ...     person_set = getUtility(IPersonSet)
-    ...     person, ignored = person_set.createPersonAndEmail(
-    ...         email, UNKNOWN)
-    ...     # Switch the email to avoid conflicts when generating clashing names
-    ...     email_address_set = getUtility(IEmailAddressSet)
-    ...     global _counter
-    ...     for email_address in email_address_set.getByPerson(person):
-    ...         removeSecurityProxy(email_address).email = (
-    ...             'nodupe%d@xxxxxxxxxxx' % _counter)
-    ...         _counter += 1
-    ...     flush_database_updates()
-    ...     assert email_address_set.getByEmail(email) is None, 'Oops'
-    ...     return person
-
-The email address is used to generate nicknames. If domain is used to
-attempt to avoid clashes.
-
-    >>> new_person('bongo@xxxxxxxxxxx').name
-    u'bongo'
-    >>> new_person('bongo@xxxxxxxxxxx').name
-    u'bongo-example'
-    >>> new_person('bongo@xxxxxxxxxxx').name
-    u'bongo-example-com'
-
-It should be nearly impossible to make the method fail even in the
-extreme cases where using all components of the domain name fails.
-If nicknames still clash, random suffixes are tried:
-
-    >>> new_person('bongo@xxxxxxxxxxx').name
-    u'bongo-example-com-c'
-
-Random prefixes are tried:
-
-    >>> new_person('bongo@xxxxxxxxxxx').name
-    u'q-bongo-example-com'
-
-And random mutations are tried:
-
-    >>> new_person('bongo@xxxxxxxxxxx').name
-    u'bongo-examale-com'
-
-The only way it can fail is if some idiot admin goes and registers a
-'match-everything' pattern in the blacklist:
-
-    >>> from canonical.database.sqlbase import cursor
-    >>> cur = cursor()
-    >>> cur.execute("INSERT INTO NameBlacklist (regexp) VALUES ('.')")
-    >>> generate_nick("foo@xxxxxxxxxxx")
-    Traceback (most recent call last):
-    ...
-    NicknameGenerationError: No nickname could be generated...

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2011-05-18 03:36:29 +0000
+++ lib/lp/registry/model/person.py	2011-05-23 19:19:28 +0000
@@ -4541,9 +4541,8 @@
         raise NicknameGenerationError("%s is not a valid email address"
                                       % email_addr)
 
-    user, domain = re.match("^(\S+)@(\S+)$", email_addr).groups()
+    user = re.match("^(\S+)@(\S+)$", email_addr).groups()[0]
     user = user.replace(".", "-").replace("_", "-")
-    domain_parts = domain.split(".")
 
     person_set = PersonSet()
 
@@ -4561,11 +4560,6 @@
     if _valid_nick(generated_nick):
         return generated_nick
 
-    for domain_part in domain_parts:
-        generated_nick = sanitize_name(generated_nick + "-" + domain_part)
-        if _valid_nick(generated_nick):
-            return generated_nick
-
     # We seed the random number generator so we get consistent results,
     # making the algorithm repeatable and thus testable.
     random_state = random.getstate()

=== added file 'lib/lp/registry/tests/test_nickname.py'
--- lib/lp/registry/tests/test_nickname.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_nickname.py	2011-05-23 19:19:28 +0000
@@ -0,0 +1,59 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for nickname generation"""
+
+__metaclass__ = type
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.registry.model.person import (
+    generate_nick,
+    NicknameGenerationError,
+    )
+from lp.testing import TestCaseWithFactory
+
+
+class TestNicknameGeneration(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_rejects_invalid_emails(self):
+        # generate_nick rejects invalid email addresses
+        self.assertRaises(
+            NicknameGenerationError,
+            generate_nick,
+            'foo@example@com')
+
+    def test_uses_email_address(self):
+        # generate_nick uses the first part of the email address to create
+        # the nick.
+        nick = generate_nick('bar@xxxxxxxxxxx')
+        self.assertEqual('bar', nick)
+
+    def test_does_not_start_nick_with_symbols(self):
+        # If an email starts with symbols, generate_nick still creates a
+        # valid nick that doesn't start with symbols.
+        nick = generate_nick('---bar@xxxxxxxxxxx')
+        self.assertEqual('bar', nick)
+
+    def test_enforces_minimum_length(self):
+        # Nicks must be a minimum of four characters. generate_nick creates
+        # nicks over a that length.
+        nick = generate_nick('i@xxxxxxxxxxx')
+        self.assertEqual('i-5', nick)
+
+    def test_can_create_noncolliding_nicknames(self):
+        # Given the same email address, generate_nick doesn't recreate the
+        # same nick once that nick is used.
+        self._useNicknames(['bar'])
+        nick = generate_nick('bar@xxxxxxxxxxx')
+        self.assertEqual('bar-c', nick)
+
+        self._useNicknames(['bar-c'])
+        self.assertEqual('bar-c', nick)
+
+    def _useNicknames(self, nicknames):
+        # Helper method to consume a nickname
+        for nick in nicknames:
+            self.factory.makePerson(name=nick)
+        #flush_database_updates()


Follow ups