From 3c51ae6008104903823006667f2441758fd2ffa5 Mon Sep 17 00:00:00 2001
From: Andreas Ellewsen <ae@uio.no>
Date: Mon, 14 Feb 2022 16:27:15 +0100
Subject: [PATCH] Handle wrong person following invitation link

If someone that already exists in greg follows an invite, we are kind
enough to give the new role to the existing person. This introduced a
security risk if the invitation was actually meant for someone else.

Because of this situation, we introduce a security mechanism where we
disable the invitationlink if the name of the existing person is too
different from the name used in the invitation.

Resolves: GREG-166
---
 gregui/authentication/auth_backends.py | 67 ++++++++++++++++----------
 gregui/tests/test_utils.py             |  8 ++-
 gregui/utils.py                        |  4 ++
 3 files changed, 53 insertions(+), 26 deletions(-)

diff --git a/gregui/authentication/auth_backends.py b/gregui/authentication/auth_backends.py
index 63754aae..c3ff9ef0 100644
--- a/gregui/authentication/auth_backends.py
+++ b/gregui/authentication/auth_backends.py
@@ -10,13 +10,13 @@ from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.auth.backends import BaseBackend, UserModel
 from django.core.exceptions import SuspiciousOperation
-from django.utils.timezone import make_aware
+from django.utils import timezone
 
 from mozilla_django_oidc.auth import OIDCAuthenticationBackend
 
 from greg.models import Identity, Person, Sponsor, InvitationLink
 from gregui.models import GregUserProfile
-from gregui.utils import name_diff
+from gregui.utils import name_diff_too_large
 
 logger = structlog.getLogger(__name__)
 
@@ -339,7 +339,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
             existing_nin.source = source
             existing_nin.value = new_nin
             existing_nin.verified = Identity.Verified.AUTOMATIC
-            existing_nin.verified_at = make_aware(datetime.datetime.now())
+            existing_nin.verified_at = timezone.make_aware(datetime.datetime.now())
             existing_nin.save()
             logger.info("identity_update", identity_id=existing_nin.id)
 
@@ -357,7 +357,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
                 type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER,
                 value=new_nin,
                 verified=Identity.Verified.AUTOMATIC,
-                verified_at=make_aware(datetime.datetime.now()),
+                verified_at=timezone.make_aware(datetime.datetime.now()),
             )
             nin_id.save()
             logger.info("identity_add", identity_id=nin_id.id)
@@ -399,7 +399,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
             existing_feide_id.source = source
             existing_feide_id.value = new_feide_id
             existing_feide_id.verified = Identity.Verified.AUTOMATIC
-            existing_feide_id.verified_at = make_aware(datetime.datetime.now())
+            existing_feide_id.verified_at = timezone.make_aware(datetime.datetime.now())
             existing_feide_id.save()
 
         else:
@@ -421,7 +421,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
                 type=Identity.IdentityType.FEIDE_ID,
                 value=new_feide_id,
                 verified=Identity.Verified.AUTOMATIC,
-                verified_at=make_aware(datetime.datetime.now()),
+                verified_at=timezone.make_aware(datetime.datetime.now()),
             )
             feide_id.save()
             logger.info("identity_add", identity_id=feide_id.id, feide_id=new_feide_id)
@@ -445,7 +445,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
             old_email = existing_email.value
             existing_email.value = new_email
             existing_email.verified = Identity.Verified.AUTOMATIC
-            existing_email.verified_at = make_aware(datetime.datetime.now())
+            existing_email.verified_at = timezone.make_aware(datetime.datetime.now())
             existing_email.save()
             logger.info(
                 "identity_update",
@@ -472,19 +472,19 @@ class GregOIDCBackend(ValidatingOIDCBackend):
                 type=Identity.IdentityType.FEIDE_EMAIL,
                 value=new_email,
                 verified=Identity.Verified.AUTOMATIC,
-                verified_at=make_aware(datetime.datetime.now()),
+                verified_at=timezone.make_aware(datetime.datetime.now()),
             )
             email_id.save()
             logger.info("identity_add", identity_id=email_id.id, value=new_email)
 
     def _update_person_name(
-        self, person: Person, first_name: str, last_name: str, source: str
+        self, person: Person, first_name: str, last_name: str
     ) -> None:
 
         new_combined_name = f"{first_name} {last_name}"
         existing_combined_name = f"{person.first_name} {person.last_name}"
 
-        if name_diff(existing_combined_name, new_combined_name) > 4:
+        if name_diff_too_large(existing_combined_name, new_combined_name, 4):
             logger.warning(
                 "name_diff_to_large",
                 existing_name=existing_combined_name,
@@ -518,9 +518,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
             self._update_person_feide_id(person, userinfo["userid_feide"], "feide")
         if "email" in userinfo:
             self._update_person_email(person, userinfo["email"], "feide")
-        self._update_person_name(
-            person, userinfo["first_name"], userinfo["last_name"], "feide"
-        )
+        self._update_person_name(person, userinfo["first_name"], userinfo["last_name"])
 
     def _update_sponsor_from_userinfo(self, userinfo: dict, sponsor: Sponsor) -> None:
         """Updates a sponsor object with data from OIDC."""
@@ -578,26 +576,19 @@ class GregOIDCBackend(ValidatingOIDCBackend):
     def _get_or_create_greg_user_profile(self, userinfo: dict, user: UserModel):
         """Get or create a GregUserProfile."""
 
-        def _get_invite_person(
-            session, old_person: Optional[Person]
-        ) -> Optional[Person]:
+        def _get_invitation_link(session) -> Optional[InvitationLink]:
             try:
-                invitation_link = InvitationLink.objects.get(uuid=session["invite_id"])
+                return InvitationLink.objects.get(uuid=session["invite_id"])
             except InvitationLink.DoesNotExist:
                 logger.error("invite_not_found", invite_id=session["invite_id"])
                 logger.bind(authenticating_invite=False)
-                return old_person
+                return None
 
+        def _get_invite_person(invitation_link) -> Person:
             logger.bind(authenticating_invite=True)
             role = invitation_link.invitation.role
             invite_person = role.person
             logger.info("invite_found", invite_person=invite_person.id)
-
-            if old_person and not old_person == invite_person:
-                role.person = old_person
-                role.save()
-                invite_person.delete()
-                return old_person
             return invite_person
 
         logger.bind(user=user)
@@ -618,7 +609,33 @@ class GregOIDCBackend(ValidatingOIDCBackend):
             old_person = None
             if user_profile and user_profile.person:
                 old_person = self._get_person_from_userinfo(userinfo)
-            person = _get_invite_person(session, old_person)
+            person = old_person
+            invitation_link = _get_invitation_link(session)
+            if invitation_link:
+                inv_person = _get_invite_person(invitation_link)
+                if old_person and old_person != inv_person:
+                    # Logged in person has a person object in greg but has followed an
+                    # invite.
+                    inv_name = f"{inv_person.first_name} {inv_person.last_name}"
+                    old_name = f"{old_person.first_name} {old_person.last_name}"
+                    if (
+                        not inv_person.registration_completed_date
+                        and not name_diff_too_large(old_name, inv_name, 4)
+                    ):
+                        # The name is close and the invited person has not completed
+                        # registration. Give the role to the existing person, and
+                        # delete the invited one.
+                        role = invitation_link.invitation.role
+                        role.person = old_person
+                        role.save()
+                        inv_person.delete()
+                    else:
+                        # The logged in user has gotten someone else's invitation, and
+                        # the invitation should be disabled.
+                        invitation_link.expire = timezone.now()
+                        invitation_link.save()
+                else:
+                    person = inv_person
         else:
             person = self._get_person_from_userinfo(userinfo)
             logger.bind(authenticating_invite=False)
diff --git a/gregui/tests/test_utils.py b/gregui/tests/test_utils.py
index f759f306..d8cfe531 100644
--- a/gregui/tests/test_utils.py
+++ b/gregui/tests/test_utils.py
@@ -1,4 +1,4 @@
-from gregui.utils import name_diff, restricted_damarau_levenshtein
+from gregui.utils import name_diff, name_diff_too_large, restricted_damarau_levenshtein
 
 
 def test_rdm_replace():
@@ -21,3 +21,9 @@ def test_name_longer():
 def test_name_threshold():
     """Verify continues with raised threshold"""
     assert name_diff("Abcdefgh Ijklmnop", "Qrstuvw Xyz", 100) == 10
+
+
+def test_name_too_large():
+    """Verify boolean version responds with expected boolean"""
+    assert not name_diff_too_large("Test Name", "Testing Name", 3)
+    assert name_diff_too_large("Test Name", "Testing Name", 2)
diff --git a/gregui/utils.py b/gregui/utils.py
index b08012f7..0b19c993 100644
--- a/gregui/utils.py
+++ b/gregui/utils.py
@@ -60,3 +60,7 @@ def name_diff(full_name1: str, full_name2: str, threshold: int = 2) -> int:
         if total_difference > threshold:
             break
     return total_difference
+
+
+def name_diff_too_large(full_name1: str, full_name2: str, threshold: int) -> bool:
+    return name_diff(full_name1, full_name2, threshold) > threshold
-- 
GitLab