diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java index b45f070c..744a9342 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java @@ -40,12 +40,12 @@ public class HsOfficeContactController implements HsOfficeContactsApi { final String currentSubject, final String assumedRoles, final String caption, - final String emailAddress) { + final String emailAddressRegEx) { context.define(currentSubject, assumedRoles); - validate("caption, emailAddress").atMaxOne(caption, emailAddress); - final var entities = emailAddress != null - ? contactRepo.findContactByEmailAddress(emailAddress) + validate("caption, emailAddress").atMaxOne(caption, emailAddressRegEx); + final var entities = emailAddressRegEx != null + ? contactRepo.findContactByEmailAddressRegEx(emailAddressRegEx) : contactRepo.findContactByOptionalCaptionLike(caption); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepository.java index 51c3990e..924f3277 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepository.java @@ -4,6 +4,7 @@ import io.micrometer.core.annotation.Timed; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import jakarta.validation.ValidationException; import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.Optional; @@ -23,14 +24,22 @@ public interface HsOfficeContactRbacRepository extends Repository findContactByOptionalCaptionLike(String caption); @Query(value = """ - SELECT c.* FROM hs_office.contact_rv c - WHERE jsonb_path_exists(c.emailaddresses, cast(:emailAddressExpression as jsonpath)) + select c.* from hs_office.contact_rv c + where jsonb_path_exists(c.emailaddresses, cast(:emailAddressExpression as jsonpath)) """, nativeQuery = true) @Timed("app.office.contacts.repo.findContactByEmailAddressImpl.rbac") List findContactByEmailAddressImpl(final String emailAddressExpression); - default List findContactByEmailAddress(@NotNull final String emailAddress) { - return findContactByEmailAddressImpl("$.** ? (@ like_regex \"" + emailAddress.replace("%", ".*") + "\")"); + default List findContactByEmailAddressRegEx(@NotNull final String emailAddress) { + return findContactByEmailAddressImpl("$.** ? (@ like_regex \"" + emailRegEx(emailAddress) + "\")"); + } + + static String emailRegEx(@NotNull String emailAddress) { + if (emailAddress.contains("'") || emailAddress.endsWith("\\") ) { + throw new ValidationException( + "emailAddress contains invalid characters: " + emailAddress); + } + return emailAddress.replace("%", ".*"); // the JSON-matcher in PostgreSQL needs a wildcard } @Timed("app.office.contacts.repo.save.rbac") diff --git a/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml b/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml index 0dff9a63..022756d8 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml @@ -13,12 +13,13 @@ get: schema: type: string description: Beginning of caption to filter the results. - - name: emailAddress + - name: emailAddressRegEx in: query required: false schema: type: string - description: Beginning of email-address to filter the results, use '%' as wildcard. + description: + Regular-expression for an email-address to filter the results. responses: "200": description: OK diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryIntegrationTest.java index 5b59f3b6..6a7f45e3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryIntegrationTest.java @@ -193,7 +193,7 @@ class HsOfficeContactRbacRepositoryIntegrationTest extends ContextBasedTestWithC context("superuser-alex@hostsharing.net", null); // when - final var result = contactRepo.findContactByEmailAddress("%@secondcontact.example.com"); + final var result = contactRepo.findContactByEmailAddressRegEx("@secondcontact.example.com"); // then exactlyTheseContactsAreReturned(result, "second contact"); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryUnitTest.java new file mode 100644 index 00000000..d6c6574f --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryUnitTest.java @@ -0,0 +1,29 @@ +package net.hostsharing.hsadminng.hs.office.contact; + +import org.junit.jupiter.api.Test; + +import jakarta.validation.ValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class HsOfficeContactRbacRepositoryUnitTest { + + @Test + void rejectsSingleQuoteInEmailAddressRegEx() { + final var throwable = catchThrowable( () -> + HsOfficeContactRbacRepository.emailRegEx("target@'example.org") + ); + + assertThat(throwable).isInstanceOf(ValidationException.class); + } + + @Test + void rejectsTrailingBackslashInEmailAddressRegEx() { + final var throwable = catchThrowable( () -> + HsOfficeContactRbacRepository.emailRegEx("target@example.org\\") + ); + + assertThat(throwable).isInstanceOf(ValidationException.class); + } +}