From 5e48e38b1d1e38cd01c9d6e98be25b0fb179f87e Mon Sep 17 00:00:00 2001 From: Etienne Poirier <33007955+epeartree@users.noreply.github.com> Date: Wed, 28 Aug 2024 07:20:49 -0400 Subject: [PATCH] 6203 subscriptionvalidatinginterceptor allows invalid url for rest hook endpoint (#6225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use SearchParamater validator in package installer (#6112) * Ensure ' ' is treated as '+' in timezones with offsets. (#6115) * Use lockless mode when adding index on Azure Sql server (#6100) * Use lockless mode when adding index on Azure Sql server Use try-catch for Online add-index on Sql Server. This avoids having to map out the entire matrix of Sql Server product names and ONLINE index support. Warnings in docs, and cleanups * make consent service dont call willSeeResource on children if parent resource is AUTHORIZED or REJECT (#6127) * fix hfj search migration task (#6143) * fix migration task * changelog * changelog * code review * spotless --------- Co-authored-by: jdar * Enhance migration for MSSQL to change the collation for HFJ_RESOURCE.FHIR_ID to case sensitive (#6135) * MSSQL: Migrate HFJ_RESOURCE.FHIR_ID to new collation: SQL_Latin1_General_CP1_CS_AS * Spotless. * Enhance test. Fix case in ResourceSearchView to defend against future migration to case insensitive collation. * Remove TODOs. Add comment to ResourceSearchView explaining why all columns are uppercase. Changelog. * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6146-mssql-hfj-resource-fhir-id-colllation.yaml Code reviewer suggestion Co-authored-by: Michael Buckley * Code review fixes: Make changes conditional on the collation including _CI_, otherwise, leave it alone. --------- Co-authored-by: Michael Buckley * Common API for FHIR Data Access (#6141) * Add initial interface for common FHIR API * Fix formatting * Update javadocs * Address code review comments * Add path value to _id search parameter and other missing search param… (#6128) * Add path value to _id search parameter and other missing search parameters to IAnyResource. * Adjust tests and remove now unnecessary addition of meta parameters which are now provided by IAnyResource * Revert unneeded change * _security param is not token but uri * Add tests for new defined resource-level standard parameters * Adjust test --------- Co-authored-by: juan.marchionatto * update to online (#6157) * SEARCH_UUID should be non-null (#6165) Avoid using constants in migrations because it creates false history. * Handle 400 and 404 codes returned by remote terminology operation. (#6151) * Handle 400 and 404 codes returned by remote terminology operation. * Some simplification * Adjust changelog * Add a comment to explain alternate solution which can be reused. * fix concepts with no display element for $apply-codesystem-delta-add and $apply-codesystem-delta-remove (#6164) * allow transaction with update conditional urls (#6155) * Revert "Add path value to _id search parameter and other missing search param…" (#6171) This reverts commit 2275eba1a0f379b6ccb2d1b467fa22bd9febcf54. * 7 2 2 mb (#6160) * Enhance RuleBuilder code to support multiple instances (#5852) * Overhaul bulk export permissions. * Overhaul bulk export permissions. * Small tweak to rule builder. * Cleanup validation. * Cleanup validation. * Code review feedback. * Postgres terminology service hard coded column names migration (#5866) * updating parent pids column name * updating name of the fullTestField Search * updating name of the fullTestField Search * fixing typo. * failing test. * - Moving FullTextField annotation from getter method and adding it to the newly added VC property of the entity; - reverting the name of the FullTextField entity to its previous name of 'myParentPids'; - reverting the name of the lucene index to search on in the terminology service. - updating the changelog; * making spotless happy --------- Co-authored-by: peartree * 5879 back porting fix for issue 5877 (attempting to update a tokenparam with a value greater than 200 characters raises an sqlexception) to release rel_7_2 (#5881) * initial failing test. * solution * adding changelog * spotless * moving changelog from 7_4_0 to 7_2_0 and deleting 7_4_0 folder. --------- Co-authored-by: peartree * Expose BaseRequestPartitionHelperSvc validateAndNormalize methods (#5811) * Expose BaseRequestPartitionHelperSvc validate and normalize methods * Compilation errors * change mock test to jpa test * change mock test to jpa test * validateAndNormalizePartitionIds * validateAndNormalizePartitionNames * validateAndNormalizePartitionIds validation + bug fix * validateAndNormalizePartitionNames validation * fix test * version bump * Ensure a non-numeric FHIR ID doesn't result in a NumberFormatException when processing survivorship rules (#5883) * Add failing test as well as commented out potential solution. * Fix for NumberFormatException. * Add conditional test for survivorship rules. * Spotless. * Add changelog. * Code review feedback. * updating documentation (#5889) * Ensure temp file ends with "." and then suffix. (#5894) * bugfix to https://github.com/hapifhir/hapi-fhir-jpaserver-starter/issues/675 (#5892) Co-authored-by: Jens Kristian Villadsen * Enhance mdm interceptor (#5899) * Add MDM Transaction Context for further downstream processing giving interceptors a better chance of figuring out what happened. * Added javadoc * Cahngelog * spotless --------- Co-authored-by: Jens Kristian Villadsen * Fix BaseHapiFhirResourceDao $meta method to use HapiTransactionService instead of @Transaction (#5896) * Try making ResourceTable.myTags EAGER instead of LAZY and see if it breaks anything. * Try making ResourceTable.myTags EAGER instead of LAZY and see if it breaks anything. * Ensure BaseHapiFhirResourceDao#metaGetOperation uses HapiTransactionService instead of @Transactional in order to resolve megascale $meta bug. * Add changelog. * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5898-ld-megascale-meta-operation-fails-hapi-0389.yaml Commit code reviewer suggestion. Co-authored-by: Tadgh --------- Co-authored-by: Tadgh * Fix query chained on sort bug where we over-filter results (#5903) * Failing test. * Ensure test cleanup doesn't fail by deleting Patients before Practitioners. * Implement fix. * Spotless. * Clean up unit test and add changelog. Fix unit test. * Fix changelog file. * Apply suggestions from code review Apply code review suggestions. Co-authored-by: Michael Buckley * Spotless --------- Co-authored-by: Michael Buckley * cve fix (#5906) Co-authored-by: Long Ma * Fixing issues with postgres LOB migration. (#5895) * Fixing issues with postgres LOB migration. * addressing code review comments for audit/transaction logs. * test and implementation for BinaryStorageEntity migration post code review. * test and implementation for BinaryStorageEntity migration post code review. * test and implementation for TermConcept migration post code review. * applying spotless * test and implementation for TermConceptProperty migration post code review. * test and implementation for TermValueSetConcept migration post code review. * fixing migration version * fixing migration task * changelog * fixing changelog * Minor renames * addressing comments and suggestions from second code review. * passing tests * fixing more tests --------- Co-authored-by: peartree Co-authored-by: Tadgh * 6051 bulk export security errors (#5915) * Enhance RuleBuilder code to support multiple instances (#5852) * Overhaul bulk export permissions. * Overhaul bulk export permissions. * Small tweak to rule builder. * Cleanup validation. * Cleanup validation. * Code review feedback. * Postgres terminology service hard coded column names migration (#5866) * updating parent pids column name * updating name of the fullTestField Search * updating name of the fullTestField Search * fixing typo. * failing test. * - Moving FullTextField annotation from getter method and adding it to the newly added VC property of the entity; - reverting the name of the FullTextField entity to its previous name of 'myParentPids'; - reverting the name of the lucene index to search on in the terminology service. - updating the changelog; * making spotless happy --------- Co-authored-by: peartree * 5879 back porting fix for issue 5877 (attempting to update a tokenparam with a value greater than 200 characters raises an sqlexception) to release rel_7_2 (#5881) * initial failing test. * solution * adding changelog * spotless * moving changelog from 7_4_0 to 7_2_0 and deleting 7_4_0 folder. --------- Co-authored-by: peartree * Expose BaseRequestPartitionHelperSvc validateAndNormalize methods (#5811) * Expose BaseRequestPartitionHelperSvc validate and normalize methods * Compilation errors * change mock test to jpa test * change mock test to jpa test * validateAndNormalizePartitionIds * validateAndNormalizePartitionNames * validateAndNormalizePartitionIds validation + bug fix * validateAndNormalizePartitionNames validation * fix test * version bump * Ensure a non-numeric FHIR ID doesn't result in a NumberFormatException when processing survivorship rules (#5883) * Add failing test as well as commented out potential solution. * Fix for NumberFormatException. * Add conditional test for survivorship rules. * Spotless. * Add changelog. * Code review feedback. * updating documentation (#5889) * Ensure temp file ends with "." and then suffix. (#5894) * bugfix to https://github.com/hapifhir/hapi-fhir-jpaserver-starter/issues/675 (#5892) Co-authored-by: Jens Kristian Villadsen * Enhance mdm interceptor (#5899) * Add MDM Transaction Context for further downstream processing giving interceptors a better chance of figuring out what happened. * Added javadoc * Cahngelog * spotless --------- Co-authored-by: Jens Kristian Villadsen * Fix BaseHapiFhirResourceDao $meta method to use HapiTransactionService instead of @Transaction (#5896) * Try making ResourceTable.myTags EAGER instead of LAZY and see if it breaks anything. * Try making ResourceTable.myTags EAGER instead of LAZY and see if it breaks anything. * Ensure BaseHapiFhirResourceDao#metaGetOperation uses HapiTransactionService instead of @Transactional in order to resolve megascale $meta bug. * Add changelog. * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5898-ld-megascale-meta-operation-fails-hapi-0389.yaml Commit code reviewer suggestion. Co-authored-by: Tadgh --------- Co-authored-by: Tadgh * Fix query chained on sort bug where we over-filter results (#5903) * Failing test. * Ensure test cleanup doesn't fail by deleting Patients before Practitioners. * Implement fix. * Spotless. * Clean up unit test and add changelog. Fix unit test. * Fix changelog file. * Apply suggestions from code review Apply code review suggestions. Co-authored-by: Michael Buckley * Spotless --------- Co-authored-by: Michael Buckley * cve fix (#5906) Co-authored-by: Long Ma * Fixing issues with postgres LOB migration. (#5895) * Fixing issues with postgres LOB migration. * addressing code review comments for audit/transaction logs. * test and implementation for BinaryStorageEntity migration post code review. * test and implementation for BinaryStorageEntity migration post code review. * test and implementation for TermConcept migration post code review. * applying spotless * test and implementation for TermConceptProperty migration post code review. * test and implementation for TermValueSetConcept migration post code review. * fixing migration version * fixing migration task * changelog * fixing changelog * Minor renames * addressing comments and suggestions from second code review. * passing tests * fixing more tests --------- Co-authored-by: peartree Co-authored-by: Tadgh * refactor bulk export rule, add concept of appliestoallpatients, fix tests * spotless * Cahgnelog, tests * more tests * refactor style checks --------- Co-authored-by: Luke deGruchy Co-authored-by: Etienne Poirier <33007955+epeartree@users.noreply.github.com> Co-authored-by: peartree Co-authored-by: Nathan Doef Co-authored-by: TipzCM Co-authored-by: dotasek Co-authored-by: Jens Kristian Villadsen Co-authored-by: Michael Buckley Co-authored-by: longma1 <32119004+longma1@users.noreply.github.com> Co-authored-by: Long Ma * Convert a few nulls to aggressive denies * Change chain sort syntax for MS SQL (#5917) * Change sort type on chains * Change sort type on chains * Test for MS SQL * Comments * Version bump * Updating version to: 7.2.1 post release. * Fix queries with chained sort with Lucene by checking supported SortSpecs (#5958) * First commit with very rough solution. * Solidify solutions for both requirements. Add new tests. Enhance others. * Spotless. * Add new chained sort spec algorithm. Add new Msg.codes. Finalize tests. Update docs. Add changelog. * pom remove the snapshot * Updating version to: 7.2.2 post release. * cherry-picked pr 6051 * changelog fix * cherry-picked 6027 * docs and changelog * merge fix for issue with infinite cache refresh loop * Use lockless mode when adding index on Azure Sql server (#6100) (#6129) * Use lockless mode when adding index on Azure Sql server Use try-catch for Online add-index on Sql Server. This avoids having to map out the entire matrix of Sql Server product names and ONLINE index support. Warnings in docs, and cleanups * added fix for 6133 * failing Test * Add fix * spotless * Remove useless file * Fix claeaner * cleanup * Remove dead class * Changelog * test description * Add test. Fix broken logic. * fix quantity search parameter test to pass * reverted test testDirectPathWholeResourceNotIndexedWorks in FhirResourceDaoR4SearchWithElasticSearchIT * spotless * cleanup mistake during merge * added missing imports * fix more mergeback oopsies * bump to 7.3.13-snapshot --------- Co-authored-by: Luke deGruchy Co-authored-by: Etienne Poirier <33007955+epeartree@users.noreply.github.com> Co-authored-by: peartree Co-authored-by: Nathan Doef Co-authored-by: TipzCM Co-authored-by: dotasek Co-authored-by: Jens Kristian Villadsen Co-authored-by: Tadgh Co-authored-by: Michael Buckley Co-authored-by: Long Ma Co-authored-by: markiantorno * Patient validate operation with remote terminology service enabled returns 400 bad request (#6124) * Patient $validate operation with Remote Terminology Service enabled returns 400 Bad Request - failing test * Patient $validate operation with Remote Terminology Service enabled returns 400 Bad Request - implementation * - Changing method accessibility from default to public to allow method overwriting. (#6172) Co-authored-by: peartree * applying Taha Attari's fix on branch merging to rel_7_4 (#6177) Co-authored-by: peartree * Automated Migration Testing (HAPI-FHIR) V7_4_0 (#6170) * Automated Migration Testing (HAPI-FHIR) - updated test migration scripts for 7_4_0 * Automated Migration Testing (HAPI-FHIR) - updated test migration scripts for 7_2_0 * To provide the target resource partitionId and partitionDate in the resourceLinlk (#6149) * initial POC. * addressing comments from first code review * Adding tests * adding changelog and spotless * fixing tests * spotless --------- Co-authored-by: peartree * applying patch (#6190) Co-authored-by: peartree * cve for 08 release (#6197) Co-authored-by: Long Ma * Search param path missing for _id param (#6175) * Add path tp _id search param and definitions for _lastUpdated _tag, _profile and _security * Add tests and changelog * Increase snapshot version * Irrelevant change to force new build --------- Co-authored-by: juan.marchionatto * Reverting to core fhir-test-cases 1.1.14; (#6194) re-enabling FhirPatchCoreTest Co-authored-by: peartree * initial failing test * providing capability to tailor resthook endpoint url validation through supplied regex * adding necessary import. * conformance with spotless * adding tests * adding test for payload * spotless * adding changelogs * passing all tests * passing all tests * no-op commit to kickstart the pipeline. * - slight modification to the solution; - spotless; * pre-code review submission. * moving validator instantiation to the submitter configuration so it is create along side the subscriptionValidatorInterceptor * last min tweeking before submission for review. * fixing spotless post merging in master --------- Co-authored-by: Emre Dincturk <74370953+mrdnctrk@users.noreply.github.com> Co-authored-by: Luke deGruchy Co-authored-by: Michael Buckley Co-authored-by: jdar8 <69840459+jdar8@users.noreply.github.com> Co-authored-by: jdar Co-authored-by: Luke deGruchy Co-authored-by: JP Co-authored-by: jmarchionatto <60409882+jmarchionatto@users.noreply.github.com> Co-authored-by: juan.marchionatto Co-authored-by: TipzCM Co-authored-by: Martha Mitran Co-authored-by: longma1 <32119004+longma1@users.noreply.github.com> Co-authored-by: peartree Co-authored-by: Nathan Doef Co-authored-by: dotasek Co-authored-by: Jens Kristian Villadsen Co-authored-by: Tadgh Co-authored-by: Long Ma Co-authored-by: markiantorno Co-authored-by: volodymyr-korzh <132366313+volodymyr-korzh@users.noreply.github.com> --- ...idator-interceptor-allows-invalid-urls.yml | 5 + .../config/BaseSubscriptionSettings.java | 39 ++++++ .../config/SubscriptionConfig.java | 2 +- .../config/SubscriptionSubmitterConfig.java | 27 ++++ .../SubscriptionValidatingInterceptor.java | 40 +++--- .../validator/IChannelTypeValidator.java | 11 ++ .../RegexEndpointUrlValidationStrategy.java | 33 +++++ .../validator/RestHookChannelValidator.java | 77 +++++++++++ ...bscriptionChannelTypeValidatorFactory.java | 46 +++++++ .../SubscriptionQueryValidator.java | 2 +- .../jpa/topic/SubscriptionTopicConfig.java | 2 +- ...ubscriptionTopicValidatingInterceptor.java | 2 +- .../matching/DaoSubscriptionMatcherTest.java | 2 +- ...SubscriptionValidatingInterceptorTest.java | 76 ++++++++-- .../RestHookChannelValidatorTest.java | 130 ++++++++++++++++++ ...SubscriptionValidatingInterceptorTest.java | 46 ++++--- 16 files changed, 482 insertions(+), 58 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6203-subscription-validator-interceptor-allows-invalid-urls.yml create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/IChannelTypeValidator.java create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RegexEndpointUrlValidationStrategy.java create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RestHookChannelValidator.java create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/SubscriptionChannelTypeValidatorFactory.java rename hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/{ => validator}/SubscriptionQueryValidator.java (97%) create mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RestHookChannelValidatorTest.java diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6203-subscription-validator-interceptor-allows-invalid-urls.yml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6203-subscription-validator-interceptor-allows-invalid-urls.yml new file mode 100644 index 00000000000..8cd77cb86eb --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6203-subscription-validator-interceptor-allows-invalid-urls.yml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 6203 +title: "Previously, the SubscriptionValidatingInterceptor would allow the creation/update of a REST hook subscription +where the endpoint URL property is not prefixed with http[s]. This issue is fixed." diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/BaseSubscriptionSettings.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/BaseSubscriptionSettings.java index 1eba93e2487..fdc8ab333c6 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/BaseSubscriptionSettings.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/BaseSubscriptionSettings.java @@ -27,9 +27,13 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + public abstract class BaseSubscriptionSettings { public static final String DEFAULT_EMAIL_FROM_ADDRESS = "noreply@unknown.com"; public static final String DEFAULT_WEBSOCKET_CONTEXT_PATH = "/websocket"; + public static final String DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX = + "((((http?|https?)://))([-%()_.!~*';/?:@&=+$,A-Za-z0-9])+)"; private final Set mySupportedSubscriptionTypes = new HashSet<>(); private String myEmailFromAddress = DEFAULT_EMAIL_FROM_ADDRESS; @@ -45,6 +49,13 @@ public abstract class BaseSubscriptionSettings { */ private boolean myAllowOnlyInMemorySubscriptions = false; + /** + * @since 7.6.0 + * + * Regex To perform validation on the endpoint URL for Subscription of type RESTHOOK. + */ + private String myRestHookEndpointUrlValidationRegex = DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX; + /** * This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted * to the server matching these types will be activated. @@ -235,4 +246,32 @@ public abstract class BaseSubscriptionSettings { public void setTriggerSubscriptionsForNonVersioningChanges(boolean theTriggerSubscriptionsForNonVersioningChanges) { myTriggerSubscriptionsForNonVersioningChanges = theTriggerSubscriptionsForNonVersioningChanges; } + + /** + * Provides the regex expression to perform endpoint URL validation If rest-hook subscriptions are supported. + * Default value is {@link #DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX}. + * @since 7.6.0 + */ + public String getRestHookEndpointUrlValidationRegex() { + return myRestHookEndpointUrlValidationRegex; + } + + /** + * Configure the regex expression that will be used to validate the endpoint URL. + * Set to NULL or EMPTY for no endpoint URL validation. + * + * @since 7.6.0 + */ + public void setRestHookEndpointUrlValidationRegex(String theRestHookEndpointUrlValidationgRegex) { + myRestHookEndpointUrlValidationRegex = theRestHookEndpointUrlValidationgRegex; + } + + /** + * Whether an endpoint validation Regex was set for URL validation. + * + * @since 7.6.0 + */ + public boolean hasRestHookEndpointUrlValidationRegex() { + return isNotBlank(myRestHookEndpointUrlValidationRegex); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/SubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/SubscriptionConfig.java index 4d95a057c54..5b15789de96 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/SubscriptionConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/SubscriptionConfig.java @@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.subscription.config; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java index b8d95efde31..1ef64a6cfb1 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java @@ -30,6 +30,10 @@ import ca.uhn.fhir.jpa.subscription.model.config.SubscriptionModelConfig; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionValidatingInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.IChannelTypeValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RegexEndpointUrlValidationStrategy; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionChannelTypeValidatorFactory; import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc; import ca.uhn.fhir.jpa.subscription.triggering.SubscriptionTriggeringSvcImpl; @@ -43,6 +47,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; +import java.util.List; + +import static ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator.noOpEndpointUrlValidationStrategy; + /** * This Spring config should be imported by a system that submits resources to the * matching queue for processing @@ -103,4 +111,23 @@ public class SubscriptionSubmitterConfig { return new AsyncResourceModifiedSubmitterSvc( theIResourceModifiedMessagePersistenceSvc, theResourceModifiedConsumer); } + + @Bean + public IChannelTypeValidator restHookChannelValidator(SubscriptionSettings theSubscriptionSettings) { + RestHookChannelValidator.IEndpointUrlValidationStrategy iEndpointUrlValidationStrategy = + noOpEndpointUrlValidationStrategy; + + if (theSubscriptionSettings.hasRestHookEndpointUrlValidationRegex()) { + String endpointUrlValidationRegex = theSubscriptionSettings.getRestHookEndpointUrlValidationRegex(); + iEndpointUrlValidationStrategy = new RegexEndpointUrlValidationStrategy(endpointUrlValidationRegex); + } + + return new RestHookChannelValidator(iEndpointUrlValidationStrategy); + } + + @Bean + public SubscriptionChannelTypeValidatorFactory subscriptionChannelTypeValidatorFactory( + List theValidorList) { + return new SubscriptionChannelTypeValidatorFactory(theValidorList); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java index 0e274b5fa84..c57dc761665 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java @@ -36,8 +36,10 @@ import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyE import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.IChannelTypeValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionChannelTypeValidatorFactory; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator; import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; @@ -87,6 +89,9 @@ public class SubscriptionValidatingInterceptor { @Autowired private SubscriptionQueryValidator mySubscriptionQueryValidator; + @Autowired + private SubscriptionChannelTypeValidatorFactory mySubscriptionChannelTypeValidatorFactory; + @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) public void resourcePreCreate( IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { @@ -149,7 +154,7 @@ public class SubscriptionValidatingInterceptor { break; } - validatePermissions(theSubscription, subscription, theRequestDetails, theRequestPartitionId, thePointcut); + validatePermissions(theSubscription, theRequestDetails, theRequestPartitionId, thePointcut); mySubscriptionCanonicalizer.setMatchingStrategyTag(theSubscription, null); @@ -167,7 +172,7 @@ public class SubscriptionValidatingInterceptor { try { SubscriptionMatchingStrategy strategy = mySubscriptionStrategyEvaluator.determineStrategy(subscription); - if (!(SubscriptionMatchingStrategy.IN_MEMORY == strategy) + if (SubscriptionMatchingStrategy.IN_MEMORY != strategy && mySubscriptionSettings.isOnlyAllowInMemorySubscriptions()) { throw new InvalidRequestException( Msg.code(2367) @@ -236,7 +241,6 @@ public class SubscriptionValidatingInterceptor { protected void validatePermissions( IBaseResource theSubscription, - CanonicalSubscription theCanonicalSubscription, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId, Pointcut thePointcut) { @@ -319,27 +323,11 @@ public class SubscriptionValidatingInterceptor { protected void validateChannelType(CanonicalSubscription theSubscription) { if (theSubscription.getChannelType() == null) { throw new UnprocessableEntityException(Msg.code(20) + "Subscription.channel.type must be populated"); - } else if (theSubscription.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { - validateChannelPayload(theSubscription); - validateChannelEndpoint(theSubscription); } - } - @SuppressWarnings("WeakerAccess") - protected void validateChannelEndpoint(CanonicalSubscription theResource) { - if (isBlank(theResource.getEndpointUrl())) { - throw new UnprocessableEntityException( - Msg.code(21) + "Rest-hook subscriptions must have Subscription.channel.endpoint defined"); - } - } - - @SuppressWarnings("WeakerAccess") - protected void validateChannelPayload(CanonicalSubscription theResource) { - if (!isBlank(theResource.getPayloadString()) - && EncodingEnum.forContentType(theResource.getPayloadString()) == null) { - throw new UnprocessableEntityException(Msg.code(1985) + "Invalid value for Subscription.channel.payload: " - + theResource.getPayloadString()); - } + IChannelTypeValidator iChannelTypeValidator = + mySubscriptionChannelTypeValidatorFactory.getValidatorForChannelType(theSubscription.getChannelType()); + iChannelTypeValidator.validateChannelType(theSubscription); } @SuppressWarnings("WeakerAccess") @@ -371,4 +359,10 @@ public class SubscriptionValidatingInterceptor { mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator; mySubscriptionQueryValidator = new SubscriptionQueryValidator(myDaoRegistry, theSubscriptionStrategyEvaluator); } + + @VisibleForTesting + public void setSubscriptionChannelTypeValidatorFactoryForUnitTest( + SubscriptionChannelTypeValidatorFactory theSubscriptionChannelTypeValidatorFactory) { + mySubscriptionChannelTypeValidatorFactory = theSubscriptionChannelTypeValidatorFactory; + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/IChannelTypeValidator.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/IChannelTypeValidator.java new file mode 100644 index 00000000000..480e5546697 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/IChannelTypeValidator.java @@ -0,0 +1,11 @@ +package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator; + +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; + +public interface IChannelTypeValidator { + + void validateChannelType(CanonicalSubscription theSubscription); + + CanonicalSubscriptionChannelType getSubscriptionChannelType(); +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RegexEndpointUrlValidationStrategy.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RegexEndpointUrlValidationStrategy.java new file mode 100644 index 00000000000..cb1b97d5858 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RegexEndpointUrlValidationStrategy.java @@ -0,0 +1,33 @@ +package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import jakarta.annotation.Nonnull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class RegexEndpointUrlValidationStrategy implements RestHookChannelValidator.IEndpointUrlValidationStrategy { + + private final Pattern myEndpointUrlValidationPattern; + + public RegexEndpointUrlValidationStrategy(@Nonnull String theEndpointUrlValidationRegex) { + try { + myEndpointUrlValidationPattern = Pattern.compile(theEndpointUrlValidationRegex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException( + Msg.code(2546) + " invalid synthax for provided regex " + theEndpointUrlValidationRegex); + } + } + + @Override + public void validateEndpointUrl(String theEndpointUrl) { + Matcher matcher = myEndpointUrlValidationPattern.matcher(theEndpointUrl); + + if (!matcher.matches()) { + throw new UnprocessableEntityException( + Msg.code(2545) + "Failed validation for endpoint URL: " + theEndpointUrl); + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RestHookChannelValidator.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RestHookChannelValidator.java new file mode 100644 index 00000000000..baba7030a1a --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RestHookChannelValidator.java @@ -0,0 +1,77 @@ +package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import jakarta.annotation.Nonnull; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +/** + * + * Definition of a REST Hook channel validator that perform checks on the channel payload and endpoint URL. + * + * The channel payload will always evaluate in the same manner where endpoint URL validation can be extended beyond the + * minimal validation perform by this class. + * + * At a minimum, this class ensures that the provided URL is not blank or null. Supplemental validation(s) should be + * encapsulated into a {@link IEndpointUrlValidationStrategy} and provided with the arg constructor. + * + */ +public class RestHookChannelValidator implements IChannelTypeValidator { + + private final IEndpointUrlValidationStrategy myEndpointUrlValidationStrategy; + + /** + * Constructor for a validator where the endpoint URL will + */ + public RestHookChannelValidator() { + this(noOpEndpointUrlValidationStrategy); + } + + public RestHookChannelValidator(@Nonnull IEndpointUrlValidationStrategy theEndpointUrlValidationStrategy) { + myEndpointUrlValidationStrategy = theEndpointUrlValidationStrategy; + } + + @Override + public void validateChannelType(CanonicalSubscription theSubscription) { + validateChannelPayload(theSubscription); + validateChannelEndpoint(theSubscription); + } + + @Override + public CanonicalSubscriptionChannelType getSubscriptionChannelType() { + return CanonicalSubscriptionChannelType.RESTHOOK; + } + + protected void validateChannelEndpoint(@Nonnull CanonicalSubscription theCanonicalSubscription) { + String endpointUrl = theCanonicalSubscription.getEndpointUrl(); + + if (isBlank(endpointUrl)) { + throw new UnprocessableEntityException( + Msg.code(21) + "Rest-hook subscriptions must have Subscription.channel.endpoint defined"); + } + + myEndpointUrlValidationStrategy.validateEndpointUrl(endpointUrl); + } + + protected void validateChannelPayload(CanonicalSubscription theResource) { + if (!isBlank(theResource.getPayloadString()) + && EncodingEnum.forContentType(theResource.getPayloadString()) == null) { + throw new UnprocessableEntityException(Msg.code(1985) + "Invalid value for Subscription.channel.payload: " + + theResource.getPayloadString()); + } + } + + /** + * A concrete instantiation of this interface should provide tailored validation of an endpoint URL + * throwing {@link RuntimeException} upon validation failure. + */ + public interface IEndpointUrlValidationStrategy { + void validateEndpointUrl(String theEndpointUrl); + } + + public static final IEndpointUrlValidationStrategy noOpEndpointUrlValidationStrategy = theEndpointUrl -> {}; +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/SubscriptionChannelTypeValidatorFactory.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/SubscriptionChannelTypeValidatorFactory.java new file mode 100644 index 00000000000..a86845b70af --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/SubscriptionChannelTypeValidatorFactory.java @@ -0,0 +1,46 @@ +package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator; + +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; +import jakarta.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class SubscriptionChannelTypeValidatorFactory { + private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionChannelTypeValidatorFactory.class); + + private final Map myValidators = + new EnumMap<>(CanonicalSubscriptionChannelType.class); + + public SubscriptionChannelTypeValidatorFactory(@Nonnull List theValidorList) { + theValidorList.forEach(this::addChannelTypeValidator); + } + + public IChannelTypeValidator getValidatorForChannelType(CanonicalSubscriptionChannelType theChannelType) { + return myValidators.getOrDefault(theChannelType, getNoopValidatorForChannelType(theChannelType)); + } + + public void addChannelTypeValidator(IChannelTypeValidator theValidator) { + myValidators.put(theValidator.getSubscriptionChannelType(), theValidator); + } + + private IChannelTypeValidator getNoopValidatorForChannelType(CanonicalSubscriptionChannelType theChannelType) { + return new IChannelTypeValidator() { + @Override + public void validateChannelType(CanonicalSubscription theSubscription) { + ourLog.debug( + "No validator for channel type {} was registered, will perform no-op validation.", + theChannelType); + } + + @Override + public CanonicalSubscriptionChannelType getSubscriptionChannelType() { + return theChannelType; + } + }; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionQueryValidator.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/SubscriptionQueryValidator.java similarity index 97% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionQueryValidator.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/SubscriptionQueryValidator.java index bceda0fd400..612af3c42d7 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionQueryValidator.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/SubscriptionQueryValidator.java @@ -17,7 +17,7 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.jpa.subscription.submit.interceptor; +package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java index eb14b9af406..bdd4ccb70ed 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher; import ca.uhn.fhir.jpa.subscription.config.SubscriptionConfig; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator; import ca.uhn.fhir.jpa.util.MemoryCacheService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicValidatingInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicValidatingInterceptor.java index 801c03156fe..bf5718743ce 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicValidatingInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicValidatingInterceptor.java @@ -25,7 +25,7 @@ import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/DaoSubscriptionMatcherTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/DaoSubscriptionMatcherTest.java index 37259252b7d..f46c9670e12 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/DaoSubscriptionMatcherTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/DaoSubscriptionMatcherTest.java @@ -15,7 +15,7 @@ import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig; import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender; import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator; import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java index 5c55cfbd8a2..87f50e1b56d 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java @@ -4,15 +4,19 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer; -import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.IChannelTypeValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RegexEndpointUrlValidationStrategy; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionChannelTypeValidatorFactory; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.SimpleBundleProvider; @@ -29,11 +33,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -49,6 +55,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @@ -70,6 +78,9 @@ public class SubscriptionValidatingInterceptorTest { private IFhirResourceDao mySubscriptionTopicDao; private FhirContext myFhirContext; + @SpyBean + private SubscriptionChannelTypeValidatorFactory mySubscriptionChannelTypeValidatorFactory; + @BeforeEach public void before() { setFhirContext(FhirVersionEnum.R4B); @@ -79,8 +90,9 @@ public class SubscriptionValidatingInterceptorTest { @ParameterizedTest @MethodSource("subscriptionByFhirVersion345") public void testEmptySub(IBaseResource theSubscription) { + setFhirContext(theSubscription); + try { - setFhirContext(theSubscription); mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null); fail(); } catch (UnprocessableEntityException e) { @@ -92,8 +104,9 @@ public class SubscriptionValidatingInterceptorTest { @ParameterizedTest @MethodSource("subscriptionByFhirVersion34") // R5 subscriptions don't have criteria public void testEmptyCriteria(IBaseResource theSubscription) { + initSubscription(theSubscription); + try { - initSubscription(theSubscription); mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null); fail(); } catch (UnprocessableEntityException e) { @@ -105,9 +118,10 @@ public class SubscriptionValidatingInterceptorTest { @ParameterizedTest @MethodSource("subscriptionByFhirVersion34") public void testBadCriteria(IBaseResource theSubscription) { + initSubscription(theSubscription); + SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient"); + try { - initSubscription(theSubscription); - SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient"); mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null); fail(); } catch (UnprocessableEntityException e) { @@ -118,9 +132,10 @@ public class SubscriptionValidatingInterceptorTest { @ParameterizedTest @MethodSource("subscriptionByFhirVersion34") public void testBadChannel(IBaseResource theSubscription) { + initSubscription(theSubscription); + SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient?"); + try { - initSubscription(theSubscription); - SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient?"); mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null); fail(); } catch (UnprocessableEntityException e) { @@ -131,10 +146,11 @@ public class SubscriptionValidatingInterceptorTest { @ParameterizedTest @MethodSource("subscriptionByFhirVersion345") public void testEmptyEndpoint(IBaseResource theSubscription) { + initSubscription(theSubscription); + SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient?"); + SubscriptionUtil.setChannelType(myFhirContext, theSubscription, "message"); + try { - initSubscription(theSubscription); - SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient?"); - SubscriptionUtil.setChannelType(myFhirContext, theSubscription, "message"); mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null); fail(); } catch (UnprocessableEntityException e) { @@ -223,8 +239,27 @@ public class SubscriptionValidatingInterceptorTest { SimpleBundleProvider simpleBundleProvider = new SimpleBundleProvider(List.of(topic)); when(mySubscriptionTopicDao.search(any(), any())).thenReturn(simpleBundleProvider); mySubscriptionValidatingInterceptor.validateSubmittedSubscription(badSub, null, null, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED); + + verify(mySubscriptionChannelTypeValidatorFactory, times(1)).getValidatorForChannelType(CanonicalSubscriptionChannelType.MESSAGE); } + @ParameterizedTest + @ValueSource(strings = { + "acme.corp", + "https://acme.corp/badstuff-%%$^&& iuyi", + "ftp://acme.corp"}) + public void testRestHookEndpointValidation_whenProvidedWithBadURLs(String theBadUrl) { + try { + Subscription subscriptionWithBadEndpoint = createSubscription(); + subscriptionWithBadEndpoint.getChannel().setEndpoint(theBadUrl); + + mySubscriptionValidatingInterceptor.validateSubmittedSubscription(subscriptionWithBadEndpoint, null, null, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED); + fail(""); + } catch (Exception e) { + verify(mySubscriptionChannelTypeValidatorFactory, times(1)).getValidatorForChannelType(CanonicalSubscriptionChannelType.RESTHOOK); + assertThat(e.getMessage()).startsWith(Msg.code(2545)); + } + } private void initSubscription(IBaseResource theSubscription) { setFhirContext(theSubscription); @@ -295,9 +330,22 @@ public class SubscriptionValidatingInterceptorTest { } @Bean - SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) { + SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) { return new SubscriptionQueryValidator(theDaoRegistry, theSubscriptionStrategyEvaluator); } + + @Bean + public IChannelTypeValidator restHookChannelValidator() { + String regex = new SubscriptionSettings().getRestHookEndpointUrlValidationRegex(); + RegexEndpointUrlValidationStrategy regexEndpointUrlValidationStrategy = new RegexEndpointUrlValidationStrategy(regex); + return new RestHookChannelValidator(regexEndpointUrlValidationStrategy); + } + + @Bean + public SubscriptionChannelTypeValidatorFactory subscriptionChannelTypeValidatorFactory( + List theValidorList) { + return new SubscriptionChannelTypeValidatorFactory(theValidorList); + } } @Nonnull @@ -307,7 +355,7 @@ public class SubscriptionValidatingInterceptorTest { subscription.setCriteria("Patient?"); final Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); - channel.setEndpoint("channel"); + channel.setEndpoint("http://acme.corp/"); return subscription; } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RestHookChannelValidatorTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RestHookChannelValidatorTest.java new file mode 100644 index 00000000000..10ec7d0f5cb --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/validator/RestHookChannelValidatorTest.java @@ -0,0 +1,130 @@ +package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; +import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +import jakarta.annotation.Nonnull; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.Subscription; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.REQUESTED; +import static org.junit.jupiter.api.Assertions.fail; + +public class RestHookChannelValidatorTest { + private final FhirContext myCtx = FhirContext.forR4(); + private final SubscriptionSettings mySubscriptionSettings = new SubscriptionSettings(); + private final SubscriptionCanonicalizer mySubscriptionCanonicalizer= new SubscriptionCanonicalizer(myCtx, mySubscriptionSettings); + + private final String NO_PAYLOAD = StringUtils.EMPTY; + + @ParameterizedTest + @MethodSource("urlAndExpectedEvaluationResultProvider") + public void testRestHookChannelValidation_withUrl(String theUrl, boolean theExpectedValidationResult){ + RegexEndpointUrlValidationStrategy regexEndpointUrlValidationStrategy = new RegexEndpointUrlValidationStrategy(SubscriptionSettings.DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX); + RestHookChannelValidator restHookChannelValidator = new RestHookChannelValidator(regexEndpointUrlValidationStrategy); + + CanonicalSubscription subscription = createSubscription(theUrl, NO_PAYLOAD); + doValidateUrlAndAssert(restHookChannelValidator, subscription, theExpectedValidationResult); + } + + @ParameterizedTest + @MethodSource("urlAndExpectedEvaluationResultProviderForNoUrlValidation") + public void testRestHookChannelValidation_withNoUrlValidation(String theUrl, boolean theExpectedValidationResult){ + RestHookChannelValidator restHookChannelValidator = new RestHookChannelValidator(); + + CanonicalSubscription subscription = createSubscription(theUrl, NO_PAYLOAD); + doValidateUrlAndAssert(restHookChannelValidator, subscription, theExpectedValidationResult); + } + + @ParameterizedTest + @MethodSource("payloadAndExpectedEvaluationResultProvider") + public void testRestHookChannelValidation_withPayload(String thePayload, boolean theExpectedValidationResult){ + RestHookChannelValidator restHookChannelValidator = new RestHookChannelValidator(); + + CanonicalSubscription subscription = createSubscription("https://acme.org", thePayload); + doValidatePayloadAndAssert(restHookChannelValidator, subscription, theExpectedValidationResult); + } + + private void doValidatePayloadAndAssert(RestHookChannelValidator theRestHookChannelValidator, CanonicalSubscription theSubscription, boolean theExpectedValidationResult) { + boolean validationResult = true; + + try { + theRestHookChannelValidator.validateChannelPayload(theSubscription); + } catch (Exception e){ + validationResult = false; + } + + if( validationResult != theExpectedValidationResult){ + String message = String.format("Validation result for payload %s was expected to be %b but was %b", theSubscription.getEndpointUrl(), theExpectedValidationResult, validationResult); + fail(message); + } + } + + private void doValidateUrlAndAssert(RestHookChannelValidator theRestHookChannelValidator, CanonicalSubscription theSubscription, boolean theExpectedValidationResult) { + boolean validationResult = true; + + try { + theRestHookChannelValidator.validateChannelEndpoint(theSubscription); + } catch (Exception e){ + validationResult = false; + } + + if( validationResult != theExpectedValidationResult){ + String message = String.format("Validation result for URL %s was expected to be %b but was %b", theSubscription.getEndpointUrl(), theExpectedValidationResult, validationResult); + fail(message); + } + } + + @Nonnull + private CanonicalSubscription createSubscription(String theUrl, String thePayload) { + final Subscription subscription = new Subscription(); + subscription.setStatus(REQUESTED); + subscription.setCriteria("Patient?"); + final Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); + channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); + channel.setEndpoint(theUrl); + channel.setPayload(thePayload); + return mySubscriptionCanonicalizer.canonicalize(subscription); + } + + static Stream urlAndExpectedEvaluationResultProvider() { + return Stream.of( + Arguments.of("http://www.acme.corp/fhir", true), + Arguments.of("http://acme.corp/fhir", true), + Arguments.of("http://acme.corp:8000/fhir", true), + Arguments.of("http://acme.corp:8000/fhir/", true), + Arguments.of("http://acme.corp/fhir/", true), + Arguments.of("https://foo.bar.com", true), + Arguments.of("http://localhost:8000", true), + Arguments.of("http://localhost:8000/", true), + Arguments.of("http://localhost:8000/fhir", true), + Arguments.of("http://localhost:8000/fhir/", true), + Arguments.of("acme.corp", false), + Arguments.of("https://acme.corp/badstuff-%%$^&& iuyi", false), + Arguments.of("ftp://acme.corp", false)); + } + + static Stream urlAndExpectedEvaluationResultProviderForNoUrlValidation() { + return Stream.of( + Arguments.of(null, false), + Arguments.of("", false), + Arguments.of(" ", false), + Arguments.of("something", true)); + } + + static Stream payloadAndExpectedEvaluationResultProvider() { + return Stream.of( + Arguments.of(null, true), + Arguments.of("", true), + Arguments.of(" ", true), + Arguments.of("application/json", true), + Arguments.of("garbage/fhir", false)); + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionValidatingInterceptorTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionValidatingInterceptorTest.java index a9276b79772..bc64093cbc5 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionValidatingInterceptorTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionValidatingInterceptorTest.java @@ -1,17 +1,19 @@ package ca.uhn.fhir.jpa.subscription; -import static org.junit.jupiter.api.Assertions.assertEquals; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; -import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionValidatingInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RegexEndpointUrlValidationStrategy; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionChannelTypeValidatorFactory; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; @@ -29,9 +31,13 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; + +import static ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator.IEndpointUrlValidationStrategy; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; @@ -57,6 +63,9 @@ public class SubscriptionValidatingInterceptorTest { private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; @Mock private SubscriptionSettings mySubscriptionSettings; + + private SubscriptionChannelTypeValidatorFactory mySubscriptionChannelTypeValidatorFactory; + private SubscriptionCanonicalizer mySubscriptionCanonicalizer; @BeforeEach @@ -69,6 +78,11 @@ public class SubscriptionValidatingInterceptorTest { mySvc.setFhirContext(myCtx); mySvc.setSubscriptionSettingsForUnitTest(mySubscriptionSettings); mySvc.setRequestPartitionHelperSvcForUnitTest(myRequestPartitionHelperSvc); + + IEndpointUrlValidationStrategy iEndpointUrlValidationStrategy = new RegexEndpointUrlValidationStrategy(SubscriptionSettings.DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX); + mySubscriptionChannelTypeValidatorFactory = new SubscriptionChannelTypeValidatorFactory(List.of(new RestHookChannelValidator(iEndpointUrlValidationStrategy))); + + mySvc.setSubscriptionChannelTypeValidatorFactoryForUnitTest(mySubscriptionChannelTypeValidatorFactory); } @Test @@ -85,7 +99,7 @@ public class SubscriptionValidatingInterceptorTest { @Test public void testValidate_RestHook_Populated() { - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true); Subscription subscription = new Subscription(); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); @@ -99,7 +113,7 @@ public class SubscriptionValidatingInterceptorTest { @Test public void testValidate_RestHook_ResourceTypeNotSupported() { - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(false); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(false); Subscription subscription = new Subscription(); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); @@ -118,7 +132,7 @@ public class SubscriptionValidatingInterceptorTest { @Test public void testValidate_RestHook_MultitypeResourceTypeNotSupported() { - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(false); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(false); Subscription subscription = new Subscription(); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); @@ -137,7 +151,7 @@ public class SubscriptionValidatingInterceptorTest { @Test public void testValidate_RestHook_NoEndpoint() { - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true); Subscription subscription = new Subscription(); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); @@ -156,7 +170,7 @@ public class SubscriptionValidatingInterceptorTest { @Test public void testValidate_RestHook_NoType() { - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true); Subscription subscription = new Subscription(); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); @@ -174,7 +188,7 @@ public class SubscriptionValidatingInterceptorTest { @Test public void testValidate_RestHook_NoPayload() { - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true); Subscription subscription = new Subscription(); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); @@ -203,7 +217,7 @@ public class SubscriptionValidatingInterceptorTest { @Test public void testValidate_Cross_Partition_Subscription() { - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true); when(mySubscriptionSettings.isCrossPartitionSubscriptionEnabled()).thenReturn(true); when(myRequestPartitionHelperSvc.determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Subscription.class), eq("Subscription"))).thenReturn(RequestPartitionId.defaultPartition()); @@ -222,8 +236,8 @@ public class SubscriptionValidatingInterceptorTest { // is invalid assertDoesNotThrow(() -> mySvc.resourcePreCreate(subscription, requestDetails, null)); Mockito.verify(mySubscriptionSettings, times(1)).isCrossPartitionSubscriptionEnabled(); - Mockito.verify(myDaoRegistry, times(1)).isResourceTypeSupported(eq("Patient")); - Mockito.verify(myRequestPartitionHelperSvc, times(1)).determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Subscription.class), eq("Subscription")); + Mockito.verify(myDaoRegistry, times(1)).isResourceTypeSupported("Patient"); + Mockito.verify(myRequestPartitionHelperSvc, times(1)).determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Subscription.class),eq("Subscription")); } @Test @@ -275,7 +289,7 @@ public class SubscriptionValidatingInterceptorTest { @Test public void testValidate_Cross_Partition_System_Subscription_Without_Setting() { - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true); Subscription subscription = new Subscription(); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); @@ -292,14 +306,14 @@ public class SubscriptionValidatingInterceptorTest { // is invalid mySvc.resourcePreCreate(subscription, requestDetails, null); Mockito.verify(mySubscriptionSettings, never()).isCrossPartitionSubscriptionEnabled(); - Mockito.verify(myDaoRegistry, times(1)).isResourceTypeSupported(eq("Patient")); - Mockito.verify(myRequestPartitionHelperSvc, never()).determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Patient.class), eq("Patient")); + Mockito.verify(myDaoRegistry, times(1)).isResourceTypeSupported("Patient"); + Mockito.verify(myRequestPartitionHelperSvc, never()).determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Patient.class),eq("Patient")); } @Test public void testSubscriptionUpdate() { // setup - when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true); + when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true); when(mySubscriptionSettings.isCrossPartitionSubscriptionEnabled()).thenReturn(true); lenient() .when(myRequestPartitionHelperSvc.determineReadPartitionForRequestForRead(isA(RequestDetails.class), isA(String.class), isA(IIdType.class)))