Repository: liberapay/liberapay.com

Issue:
Log in to team account
It's currently possible to log in to a team account which has an email address attached to it. This should not be allowed.
Team email isn't checked against creator's emails
It's currently possible to "lock" yourself out of your account if you haven't verified your email address and you create a team with that same email address. We should prevent that, either by checking that the addresses are different, or by requiring that a user verifies their email address before creating a team.


Discussion:

Resolving patch:
diff --git a/liberapay/exceptions.py b/liberapay/exceptions.py
--- a/liberapay/exceptions.py
+++ b/liberapay/exceptions.py
@@ -97,10 +97,12 @@ def msg(self, _):
 class ProblemChangingEmail(LazyResponse400): pass
 
 class EmailAlreadyTaken(ProblemChangingEmail):
+    code = 409
     def msg(self, _):
         return _("{0} is already connected to a different Liberapay account.", *self.args)
 
 class CannotRemovePrimaryEmail(ProblemChangingEmail):
+    code = 403
     def msg(self, _):
         return _("You cannot remove your primary email address.")
 
@@ -109,6 +111,7 @@ def msg(self, _):
         return _("The email address '{0}' is not verified.", *self.args)
 
 class TooManyEmailAddresses(ProblemChangingEmail):
+    code = 403
     def msg(self, _):
         return _("You've reached the maximum number of email addresses we allow.")
 
@@ -116,6 +119,11 @@ class BadEmailAddress(ProblemChangingEmail):
     def msg(self, _):
         return _("'{0}' is not a valid email address.", *self.args)
 
+class EmailAlreadyAttachedToSelf(ProblemChangingEmail):
+    code = 409
+    def msg(self, _):
+        return _("The email address {0} is already connected to your account.", *self.args)
+
 
 class BadPasswordSize(LazyResponse400):
     def msg(self, _):
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -31,6 +31,7 @@
     BadEmailAddress,
     BadPasswordSize,
     CannotRemovePrimaryEmail,
+    EmailAlreadyAttachedToSelf,
     EmailAlreadyTaken,
     EmailNotVerified,
     NonexistingElsewhere,
@@ -124,6 +125,15 @@ def make_active(cls, kind, username=None, password=None, cursor=None):
         return p
 
     def make_team(self, name, email=None, email_lang=None, throttle_takes=True):
+        if email and not self.email:
+            email_is_attached_to_self = self.db.one("""
+                SELECT true AS a
+                  FROM emails
+                 WHERE participant = %s
+                   AND address = %s
+            """, (self.id, email))
+            if email_is_attached_to_self:
+                raise EmailAlreadyAttachedToSelf(email)
         with self.db.get_cursor() as c:
             t = c.one("""
                 INSERT INTO participants
@@ -133,9 +143,9 @@ def make_team(self, name, email=None, email_lang=None, throttle_takes=True):
             """, (throttle_takes,))
             t.change_username(name, c)
             t.add_member(self, c)
-        if email:
-            t.set_email_lang(email_lang)
-            t.add_email(email)
+            if email:
+                t.set_email_lang(email_lang, cursor=c)
+                t.add_email(email, cursor=c)
         return t
 
     @classmethod
diff --git a/liberapay/security/authentication.py b/liberapay/security/authentication.py
--- a/liberapay/security/authentication.py
+++ b/liberapay/security/authentication.py
@@ -54,7 +54,12 @@ def sign_in_with_form_data(body, state):
         else:
             email = id
             p = Participant._from_thing('lower(email)', email.lower())
-            if p:
+            if p and p.kind == 'group':
+                state['log-in.error'] = _(
+                    "{0} is linked to a team account. It's not possible to log in as a team.",
+                    email
+                )
+            elif p:
                 p.start_session()
                 qs = {'log-in.id': p.id, 'log-in.token': p.session_token}
                 p.send_email(


Test patch:
diff --git a/tests/py/test_sign_in.py b/tests/py/test_sign_in.py
--- a/tests/py/test_sign_in.py
+++ b/tests/py/test_sign_in.py
@@ -181,6 +181,15 @@ def test_email_login_bad_token(self):
         r = self.client.GxT('/?log-in.id=%s&log-in.token=x' % alice.id)
         assert r.code == 400
 
+    def test_email_login_team_account(self):
+        email = 'team@example.net'
+        self.make_participant('team', email=email, kind='group')
+        data = {'log-in.id': email}
+        r = self.client.POST('/log-in', data, raise_immediately=False)
+        assert SESSION not in r.headers.cookie
+        Participant.dequeue_emails()
+        assert not self.get_emails()
+
 
 class TestSignIn(EmailHarness):
 
diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py
--- a/tests/py/test_teams.py
+++ b/tests/py/test_teams.py
@@ -1,5 +1,6 @@
 from __future__ import unicode_literals
 
+from liberapay.exceptions import EmailAlreadyAttachedToSelf, EmailAlreadyTaken
 from liberapay.models._mixin_team import InactiveParticipantAdded
 from liberapay.models.participant import Participant
 from liberapay.testing import Harness
@@ -111,3 +112,23 @@ def test_create_close_and_reopen_team(self):
         assert t.nmembers == 1
         assert t.status == 'active'
         assert t.goal == None
+
+    def test_create_team_with_verified_email(self):
+        alice = self.make_participant('alice')
+        email = 'bob@example.org'
+        self.make_participant('bob', email=email)
+        data = {'name': 'Team', 'email': email}
+        r = self.client.PxST('/about/teams', data, auth_as=alice)
+        assert r.code == 409
+        assert isinstance(r, EmailAlreadyTaken)
+
+    def test_create_team_with_same_unverified_email_as_creator(self):
+        alice = self.make_participant('alice')
+        email = 'alice@example.com'
+        alice.add_email(email)
+        data = {'name': 'Team', 'email': email}
+        r = self.client.PxST('/about/teams', data, auth_as=alice)
+        assert r.code == 409
+        assert isinstance(r, EmailAlreadyAttachedToSelf)
+        t = Participant.from_username('Team')
+        assert not t