Skip to content

Commit

Permalink
Add logic to ensure the invite link is correctly used when registering
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewelwell committed Aug 6, 2024
1 parent d659f4a commit aa8098b
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 15 deletions.
3 changes: 2 additions & 1 deletion api/custom_auth/oauth/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
from users.models import SignUpType

from ..constants import USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE
from ..serializers import InviteLinkValidationMixin
from .github import GithubUser
from .google import get_user_info

GOOGLE_URL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&"
UserModel = get_user_model()


class OAuthLoginSerializer(serializers.Serializer):
class OAuthLoginSerializer(InviteLinkValidationMixin, serializers.Serializer):
access_token = serializers.CharField(
required=True,
help_text="Code or access token returned from the FE interaction with the third party login provider.",
Expand Down
44 changes: 32 additions & 12 deletions api/custom_auth/serializers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import Any

from django.conf import settings
from djoser.serializers import UserCreateSerializer
from rest_framework import serializers
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import PermissionDenied
from rest_framework.validators import UniqueValidator

from organisations.invites.models import Invite
from organisations.invites.models import Invite, InviteLink
from users.auth_type import AuthType
from users.constants import DEFAULT_DELETE_ORPHAN_ORGANISATIONS_VALUE
from users.models import FFAdminUser, SignUpType
Expand All @@ -23,7 +25,35 @@ class Meta:
fields = ("key",)


class CustomUserCreateSerializer(UserCreateSerializer):
class InviteLinkValidationMixin:
invite_hash = serializers.CharField(required=False, write_only=True)

def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
attrs = super().validate(attrs)

if not settings.ALLOW_REGISTRATION_WITHOUT_INVITE:
self._validate_registration_invite(attrs)

return attrs

def _validate_registration_invite(self, attrs: dict[str, Any]) -> None:
valid = False

match attrs.get("sign_up_type"):
case SignUpType.INVITE_LINK.value:
valid = InviteLink.objects.filter(
hash=self.initial_data.get("invite_hash")
).exists()
case SignUpType.INVITE_EMAIL.value:
valid = Invite.objects.filter(
email__iexact=attrs.get("email", "").lower()
).exists()

if not valid:
raise PermissionDenied(USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE)


class CustomUserCreateSerializer(UserCreateSerializer, InviteLinkValidationMixin):
key = serializers.SerializerMethodField()

class Meta(UserCreateSerializer.Meta):
Expand Down Expand Up @@ -66,16 +96,6 @@ def get_key(instance):
token, _ = Token.objects.get_or_create(user=instance)
return token.key

def save(self, **kwargs):
if not (
settings.ALLOW_REGISTRATION_WITHOUT_INVITE
or self.validated_data.get("sign_up_type") == SignUpType.INVITE_LINK.value
or Invite.objects.filter(email=self.validated_data.get("email"))
):
raise PermissionDenied(USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE)

return super(CustomUserCreateSerializer, self).save(**kwargs)


class CustomUserDelete(serializers.Serializer):
current_password = serializers.CharField(
Expand Down
9 changes: 9 additions & 0 deletions api/tests/unit/custom_auth/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import pytest

from organisations.invites.models import InviteLink
from organisations.models import Organisation


@pytest.fixture()
def invite_link(organisation: Organisation) -> InviteLink:
return InviteLink.objects.create(organisation=organisation)
65 changes: 63 additions & 2 deletions api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import pytest
from django.test import RequestFactory
from pytest_django.fixtures import SettingsWrapper

from custom_auth.serializers import CustomUserCreateSerializer
from rest_framework.exceptions import PermissionDenied
from rest_framework.serializers import ModelSerializer

from custom_auth.constants import (
USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE,
)
from custom_auth.serializers import (
CustomUserCreateSerializer,
InviteLinkValidationMixin,
)
from organisations.invites.models import InviteLink
from users.models import FFAdminUser, SignUpType

user_dict = {
Expand Down Expand Up @@ -70,6 +80,7 @@ def test_CustomUserCreateSerializer_calls_is_authentication_method_valid_correct


def test_CustomUserCreateSerializer_allows_registration_if_sign_up_type_is_invite_link(
invite_link: InviteLink,
db: None,
settings: SettingsWrapper,
rf: RequestFactory,
Expand All @@ -80,6 +91,7 @@ def test_CustomUserCreateSerializer_allows_registration_if_sign_up_type_is_invit
data = {
**user_dict,
"sign_up_type": SignUpType.INVITE_LINK.value,
"invite_hash": invite_link.hash,
}

serializer = CustomUserCreateSerializer(
Expand All @@ -92,3 +104,52 @@ def test_CustomUserCreateSerializer_allows_registration_if_sign_up_type_is_invit

# Then
assert user


def test_invite_link_validation_mixin_validate_fails_if_invite_link_hash_not_provided(
settings: SettingsWrapper,
db: None,
) -> None:
# Given
settings.ALLOW_REGISTRATION_WITHOUT_INVITE = False

class TestSerializer(InviteLinkValidationMixin, ModelSerializer):
class Meta:
model = FFAdminUser
fields = ("sign_up_type",)

serializer = TestSerializer(data={"sign_up_type": SignUpType.INVITE_LINK.value})

# When
with pytest.raises(PermissionDenied) as exc_info:
serializer.is_valid(raise_exception=True)

# Then
assert exc_info.value.detail == USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE


def test_invite_link_validation_mixin_validate_fails_if_invite_link_hash_not_valid(
invite_link: InviteLink,
settings: SettingsWrapper,
) -> None:
# Given
settings.ALLOW_REGISTRATION_WITHOUT_INVITE = False

class TestSerializer(InviteLinkValidationMixin, ModelSerializer):
class Meta:
model = FFAdminUser
fields = ("sign_up_type",)

serializer = TestSerializer(
data={
"sign_up_type": SignUpType.INVITE_LINK.value,
"invite_hash": "invalid-hash",
}
)

# When
with pytest.raises(PermissionDenied) as exc_info:
serializer.is_valid(raise_exception=True)

# Then
assert exc_info.value.detail == USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE

0 comments on commit aa8098b

Please sign in to comment.