6203 subscriptionvalidatinginterceptor allows invalid url for rest hook endpoint (#6225)

* 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 <justin.dar@smiledigitalhealth.com>

* 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 <michaelabuckley@gmail.com>

* Code review fixes:  Make changes conditional on the collation including _CI_, otherwise, leave it alone.

---------

Co-authored-by: Michael Buckley <michaelabuckley@gmail.com>

* 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 <juan.marchionatto@smilecdr.com>

* 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 2275eba1a0.

* 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 <etienne.poirier@smilecdr.com>

* 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 <etienne.poirier@smilecdr.com>

* 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 <jenskristianvilladsen@gmail.com>

* 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 <jenskristianvilladsen@gmail.com>

* 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 <garygrantgraham@gmail.com>

---------

Co-authored-by: Tadgh <garygrantgraham@gmail.com>

* 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 <michaelabuckley@gmail.com>

* Spotless

---------

Co-authored-by: Michael Buckley <michaelabuckley@gmail.com>

* cve fix (#5906)

Co-authored-by: Long Ma <long@smilecdr.com>

* 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 <etienne.poirier@smilecdr.com>
Co-authored-by: Tadgh <garygrantgraham@gmail.com>

* 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 <etienne.poirier@smilecdr.com>

* 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 <etienne.poirier@smilecdr.com>

* 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 <jenskristianvilladsen@gmail.com>

* 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 <jenskristianvilladsen@gmail.com>

* 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 <garygrantgraham@gmail.com>

---------

Co-authored-by: Tadgh <garygrantgraham@gmail.com>

* 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 <michaelabuckley@gmail.com>

* Spotless

---------

Co-authored-by: Michael Buckley <michaelabuckley@gmail.com>

* cve fix (#5906)

Co-authored-by: Long Ma <long@smilecdr.com>

* 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 <etienne.poirier@smilecdr.com>
Co-authored-by: Tadgh <garygrantgraham@gmail.com>

* refactor bulk export rule, add concept of appliestoallpatients, fix tests

* spotless

* Cahgnelog, tests

* more tests

* refactor style checks

---------

Co-authored-by: Luke deGruchy <luke.degruchy@smilecdr.com>
Co-authored-by: Etienne Poirier <33007955+epeartree@users.noreply.github.com>
Co-authored-by: peartree <etienne.poirier@smilecdr.com>
Co-authored-by: Nathan Doef <n.doef@protonmail.com>
Co-authored-by: TipzCM <leif.stawnyczy@gmail.com>
Co-authored-by: dotasek <david.otasek@smilecdr.com>
Co-authored-by: Jens Kristian Villadsen <jenskristianvilladsen@gmail.com>
Co-authored-by: Michael Buckley <michaelabuckley@gmail.com>
Co-authored-by: longma1 <32119004+longma1@users.noreply.github.com>
Co-authored-by: Long Ma <long@smilecdr.com>

* 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 <luke.degruchy@smilecdr.com>
Co-authored-by: Etienne Poirier <33007955+epeartree@users.noreply.github.com>
Co-authored-by: peartree <etienne.poirier@smilecdr.com>
Co-authored-by: Nathan Doef <n.doef@protonmail.com>
Co-authored-by: TipzCM <leif.stawnyczy@gmail.com>
Co-authored-by: dotasek <david.otasek@smilecdr.com>
Co-authored-by: Jens Kristian Villadsen <jenskristianvilladsen@gmail.com>
Co-authored-by: Tadgh <garygrantgraham@gmail.com>
Co-authored-by: Michael Buckley <michaelabuckley@gmail.com>
Co-authored-by: Long Ma <long@smilecdr.com>
Co-authored-by: markiantorno <markiantorno@gmail.com>

* 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 <etienne.poirier@smilecdr.com>

* applying Taha Attari's fix on branch merging to rel_7_4 (#6177)

Co-authored-by: peartree <etienne.poirier@smilecdr.com>

* 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 <etienne.poirier@smilecdr.com>

* applying patch (#6190)

Co-authored-by: peartree <etienne.poirier@smilecdr.com>

* cve for 08 release (#6197)

Co-authored-by: Long Ma <long@smilecdr.com>

* 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 <juan.marchionatto@smilecdr.com>

* Reverting to core fhir-test-cases 1.1.14; (#6194)

re-enabling FhirPatchCoreTest

Co-authored-by: peartree <etienne.poirier@smilecdr.com>

* 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 <luke.degruchy@smilecdr.com>
Co-authored-by: Michael Buckley <michaelabuckley@gmail.com>
Co-authored-by: jdar8 <69840459+jdar8@users.noreply.github.com>
Co-authored-by: jdar <justin.dar@smiledigitalhealth.com>
Co-authored-by: Luke deGruchy <luke.degruchy@smiledigitalhealth.com>
Co-authored-by: JP <jonathan.i.percival@gmail.com>
Co-authored-by: jmarchionatto <60409882+jmarchionatto@users.noreply.github.com>
Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
Co-authored-by: TipzCM <leif.stawnyczy@gmail.com>
Co-authored-by: Martha Mitran <marthamitran@gmail.com>
Co-authored-by: longma1 <32119004+longma1@users.noreply.github.com>
Co-authored-by: peartree <etienne.poirier@smilecdr.com>
Co-authored-by: Nathan Doef <n.doef@protonmail.com>
Co-authored-by: dotasek <david.otasek@smilecdr.com>
Co-authored-by: Jens Kristian Villadsen <jenskristianvilladsen@gmail.com>
Co-authored-by: Tadgh <garygrantgraham@gmail.com>
Co-authored-by: Long Ma <long@smilecdr.com>
Co-authored-by: markiantorno <markiantorno@gmail.com>
Co-authored-by: volodymyr-korzh <132366313+volodymyr-korzh@users.noreply.github.com>
This commit is contained in:
Etienne Poirier 2024-08-28 07:20:49 -04:00 committed by GitHub
parent 48d8fac6ea
commit 5e48e38b1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 482 additions and 58 deletions

View File

@ -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."

View File

@ -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<Subscription.SubscriptionChannelType> 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);
}
}

View File

@ -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;

View File

@ -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<IChannelTypeValidator> theValidorList) {
return new SubscriptionChannelTypeValidatorFactory(theValidorList);
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -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 -> {};
}

View File

@ -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<CanonicalSubscriptionChannelType, IChannelTypeValidator> myValidators =
new EnumMap<>(CanonicalSubscriptionChannelType.class);
public SubscriptionChannelTypeValidatorFactory(@Nonnull List<IChannelTypeValidator> 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;
}
};
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<SubscriptionTopic> 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<IChannelTypeValidator> 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;
}
}

View File

@ -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<Arguments> 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<Arguments> urlAndExpectedEvaluationResultProviderForNoUrlValidation() {
return Stream.of(
Arguments.of(null, false),
Arguments.of("", false),
Arguments.of(" ", false),
Arguments.of("something", true));
}
static Stream<Arguments> payloadAndExpectedEvaluationResultProvider() {
return Stream.of(
Arguments.of(null, true),
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("application/json", true),
Arguments.of("garbage/fhir", false));
}
}

View File

@ -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)))