diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index f6220f1d63..1d6c1dab31 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the **Prowler API** are documented in this file. ### Fixed - Scan with no resources will not trigger legacy code for findings metadata [(#8183)](https://github.com/prowler-cloud/prowler/pull/8183) +- Invitation email comparison case-insensitive [(#8206)](https://github.com/prowler-cloud/prowler/pull/8206) ### Removed - Validation of the provider's secret type during updates [(#8197)](https://github.com/prowler-cloud/prowler/pull/8197) diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 5846212f3d..084cb41c58 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -943,6 +943,11 @@ class Invitation(RowLevelSecurityProtectedModel): null=True, ) + def save(self, *args, **kwargs): + if self.email: + self.email = self.email.strip().lower() + super().save(*args, **kwargs) + class Meta(RowLevelSecurityProtectedModel.Meta): db_table = "invitations" diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 0777267484..1ecbaf6499 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -254,7 +254,7 @@ class TestValidateInvitation: assert result == invitation mock_db.get.assert_called_once_with( - token="VALID_TOKEN", email="user@example.com" + token="VALID_TOKEN", email__iexact="user@example.com" ) def test_invitation_not_found_raises_validation_error(self): @@ -269,7 +269,7 @@ class TestValidateInvitation: "invitation_token": "Invalid invitation code." } mock_db.get.assert_called_once_with( - token="INVALID_TOKEN", email="user@example.com" + token="INVALID_TOKEN", email__iexact="user@example.com" ) def test_invitation_not_found_raises_not_found(self): @@ -284,7 +284,7 @@ class TestValidateInvitation: assert exc_info.value.detail == "Invitation is not valid." mock_db.get.assert_called_once_with( - token="INVALID_TOKEN", email="user@example.com" + token="INVALID_TOKEN", email__iexact="user@example.com" ) def test_invitation_expired(self, invitation): @@ -332,5 +332,27 @@ class TestValidateInvitation: "invitation_token": "Invalid invitation code." } mock_db.get.assert_called_once_with( - token="VALID_TOKEN", email="different@example.com" + token="VALID_TOKEN", email__iexact="different@example.com" + ) + + def test_valid_invitation_uppercase_email(self): + """Test that validate_invitation works with case-insensitive email lookup.""" + uppercase_email = "USER@example.com" + + invitation = MagicMock(spec=Invitation) + invitation.token = "VALID_TOKEN" + invitation.email = uppercase_email + invitation.expires_at = datetime.now(timezone.utc) + timedelta(days=1) + invitation.state = Invitation.State.PENDING + invitation.tenant = MagicMock() + + with patch("api.utils.Invitation.objects.using") as mock_using: + mock_db = mock_using.return_value + mock_db.get.return_value = invitation + + result = validate_invitation("VALID_TOKEN", "user@example.com") + + assert result == invitation + mock_db.get.assert_called_once_with( + token="VALID_TOKEN", email__iexact="user@example.com" ) diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index ce3f4feb52..378e050b8f 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -187,7 +187,7 @@ def validate_invitation( # Admin DB connector is used to bypass RLS protection since the invitation belongs to a tenant the user # is not a member of yet invitation = Invitation.objects.using(MainRouter.admin_db).get( - token=invitation_token, email=email + token=invitation_token, email__iexact=email ) except Invitation.DoesNotExist: if raise_not_found: