Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server side email verification validation #4631

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/openforms/formio/components/vanilla.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from openforms.config.constants import UploadFileType
from openforms.config.models import GlobalConfiguration
from openforms.submissions.attachments import temporary_upload_from_url
from openforms.submissions.models import EmailVerification
from openforms.typing import DataMapping
from openforms.utils.urls import build_absolute_uri
from openforms.validations.service import PluginValidator
Expand Down Expand Up @@ -66,6 +67,7 @@
SelectComponent,
TextFieldComponent,
)
from ..typing.base import OpenFormsConfig
from .translations import translate_options
from .utils import _normalize_pattern

Expand Down Expand Up @@ -130,16 +132,39 @@ def build_serializer_field(
return serializers.ListField(child=base) if multiple else base


class EmailVerificationValidator:
message = _("The email address {value} has not been verified yet.")
requires_context = True

def __init__(self, component_key: str):
self.component_key = component_key

def __call__(self, value: str, field: serializers.Field) -> None:
submission: Submission = field.context["submission"]
has_verification = EmailVerification.objects.filter(
submission=submission,
component_key=self.component_key,
email=value,
verified_on__isnull=False,
).exists()
if not has_verification:
raise serializers.ValidationError(
self.message.format(value=value), code="unverified"
)


@register("email")
class Email(BasePlugin):
formatter = EmailFormatter

def build_serializer_field(
self, component: Component
) -> serializers.EmailField | serializers.ListField:
extensions: OpenFormsConfig = component.get("openForms", {})
multiple = component.get("multiple", False)
validate = component.get("validate", {})
required = validate.get("required", False)
verification_required = extensions.get("requireVerification", False)

# dynamically add in more kwargs based on the component configuration
extra = {}
Expand All @@ -150,6 +175,9 @@ def build_serializer_field(
if plugin_ids := validate.get("plugins", []):
validators.append(PluginValidator(plugin_ids))

if verification_required:
validators.append(EmailVerificationValidator(component["key"]))

if validators:
extra["validators"] = validators

Expand Down
67 changes: 66 additions & 1 deletion src/openforms/formio/tests/validation/test_email.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from django.test import SimpleTestCase
from django.test import SimpleTestCase, TestCase

from rest_framework import serializers

from openforms.submissions.tests.factories import (
EmailVerificationFactory,
SubmissionFactory,
)
from openforms.typing import JSONValue
from openforms.validations.base import BasePlugin

Expand Down Expand Up @@ -94,3 +98,64 @@ def test_email_with_plugin_validator(self):
)

self.assertFalse(is_valid)


class EmailValidationDBTests(TestCase):
def test_require_email_verification(self):
submission = SubmissionFactory.create()
# create some *unrelated* verification records
ev = EmailVerificationFactory.create(
component_key="foo", email="[email protected]", verified=True
)
assert ev.submission != submission
EmailVerificationFactory(
submission=submission,
component_key="foo",
email="[email protected]",
verified=False,
)
EmailVerificationFactory(
submission=submission,
component_key="bar",
email="[email protected]",
verified=True,
)
EmailVerificationFactory(
submission=submission,
component_key="foo",
email="[email protected]",
verified=True,
)

component: Component = {
"type": "email",
"key": "foo",
"label": "Test",
"openForms": {
"requireVerification": True,
},
}
data: JSONValue = {"foo": "[email protected]"}

with self.subTest("not verified"):
is_valid, errors = validate_formio_data(
component, data, submission=submission
)

self.assertFalse(is_valid)
error = extract_error(errors, "foo")
self.assertEqual(error.code, "unverified")

with self.subTest("verified"):
# multiple successful verifications should not be a problem
EmailVerificationFactory.create_batch(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested this with multiple verification codes back and forth and with logic rules and indeed it's working as expected (since we allow this kind of behaviour).

2,
submission=submission,
component_key="foo",
email="[email protected]",
verified=True,
)

is_valid, _ = validate_formio_data(component, data, submission=submission)

self.assertTrue(is_valid)
1 change: 1 addition & 0 deletions src/openforms/formio/typing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class OpenFormsConfig(TypedDict):
maxDate: NotRequired[DateConstraintConfiguration | None]
translations: NotRequired[TranslationsDict]
components: NotRequired[AddressValidationComponents]
requireVerification: NotRequired[bool]


class OpenFormsOptionExtension(TypedDict):
Expand Down
2 changes: 2 additions & 0 deletions src/openforms/js/lang/formio/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -401,5 +401,7 @@
"{{ labels }} or {{ lastLabel }}": "{{ labels }} of {{ lastLabel }}",
"Note that any family member without a BSN will not be displayed.": "Let op, gezinsleden zonder BSN worden niet getoond.",
"invalidDatetime": "Ongeldige datum/tijd",
"Verify": "Bevestigen",
"You must verify this email address to continue.": "Om door te gaan moet je dit e-mailadres bevestigen.",
"": ""
}
Loading