fix patientId interceptor partition selection (#6236)

* fixed

* fixed

* detect uuid references earlier in the PatientIdPartitionInterceptor

* TODO
This commit is contained in:
Ken Stevens 2024-08-27 19:06:42 -04:00 committed by GitHub
parent 554bb08d01
commit 48d8fac6ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 80 additions and 17 deletions

View File

@ -37,7 +37,6 @@ public interface IRestfulClientFactory {
*/ */
public static final int DEFAULT_CONNECTION_REQUEST_TIMEOUT = 10000; public static final int DEFAULT_CONNECTION_REQUEST_TIMEOUT = 10000;
/** /**
* Default value for {@link #getConnectionTimeToLive()} * Default value for {@link #getConnectionTimeToLive()}
*/ */
@ -81,7 +80,6 @@ public interface IRestfulClientFactory {
*/ */
int getConnectTimeout(); int getConnectTimeout();
/** /**
* Gets the connection time to live, in milliseconds. This is the amount of time to keep connections alive for reuse. * Gets the connection time to live, in milliseconds. This is the amount of time to keep connections alive for reuse.
* <p> * <p>

View File

@ -120,6 +120,13 @@ public interface IIdType extends IPrimitiveType<String> {
*/ */
boolean isVersionIdPartValidLong(); boolean isVersionIdPartValidLong();
/**
* @return true if the id begins with "urn:uuid:"
*/
default boolean isUuid() {
return getValue() != null && getValue().startsWith("urn:uuid:");
}
@Override @Override
IIdType setValue(String theString); IIdType setValue(String theString);

View File

@ -105,10 +105,11 @@ public class HapiFhirCliRestfulClientFactory extends RestfulClientFactory {
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslConnectionSocketFactory) .register("https", sslConnectionSocketFactory)
.build(); .build();
connectionManager = connectionManager = new PoolingHttpClientConnectionManager(
new PoolingHttpClientConnectionManager(registry, null, null, null, getConnectionTimeToLive(), TimeUnit.MILLISECONDS); registry, null, null, null, getConnectionTimeToLive(), TimeUnit.MILLISECONDS);
} else { } else {
connectionManager = new PoolingHttpClientConnectionManager(getConnectionTimeToLive(), TimeUnit.MILLISECONDS); connectionManager =
new PoolingHttpClientConnectionManager(getConnectionTimeToLive(), TimeUnit.MILLISECONDS);
} }
connectionManager.setMaxTotal(getPoolMaxTotal()); connectionManager.setMaxTotal(getPoolMaxTotal());

View File

@ -0,0 +1,14 @@
---
type: fix
issue: 6231
title: "The PatientIdPartitionInterceptor could on rare occasion select the incorrect
partition for a resource. This has been corrected. In order for the wrong partition
to be selected, the following three things need to be true:
1) there are multiple values of a patient compartment for a resource (see https://hl7.org/fhir/R4/compartmentdefinition-patient.html)
2) a patient compartment value is a non-Patient reference
3) the search parameter of the incorrect value needs to come alphabetically before the search parameter of the correct
value.
For example, if a QuestionnaireResponse has subject Patient/123 and author Organization/456,
then since 'author' appears ahead of 'subject' alphabetically it would incorrectly determine the partition.
The fix changed the partition selection so that it now only matches on Patient references."

View File

@ -0,0 +1,36 @@
package ca.uhn.fhir.jpa.util;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.QuestionnaireResponse;
import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
public class ResourceCompartmentUtilTest extends BaseJpaR4Test {
public static final String FAMILY = "TestFamily";
private static final String ORG_NAME = "Test Organization";
@Autowired
ISearchParamExtractor mySearchParamExtractor;
@Test
public void testMultiCompartment() {
IIdType pid = createPatient(withFamily(FAMILY));
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.setSubject(new Reference(pid));
IIdType oid = createOrganization(withName(ORG_NAME));
qr.setAuthor(new Reference(oid));
Optional<String> result = ResourceCompartmentUtil.getPatientCompartmentIdentity(qr, myFhirContext, mySearchParamExtractor);
// red-green: before the bug fix, this returned the org id because "author" is alphabetically before "patient"
assertThat(result)
.isPresent()
.hasValue(pid.getIdPart());
}
}

View File

@ -39,6 +39,7 @@ import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IdType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -92,15 +93,15 @@ public class PatientIdPartitionInterceptor {
Optional<String> oCompartmentIdentity; Optional<String> oCompartmentIdentity;
if (resourceDef.getName().equals("Patient")) { if (resourceDef.getName().equals("Patient")) {
oCompartmentIdentity = IIdType idElement = theResource.getIdElement();
Optional.ofNullable(theResource.getIdElement().getIdPart()); oCompartmentIdentity = Optional.ofNullable(idElement.getIdPart());
if (oCompartmentIdentity.isEmpty()) { if (idElement.isUuid() || oCompartmentIdentity.isEmpty()) {
throw new MethodNotAllowedException( throw new MethodNotAllowedException(
Msg.code(1321) + "Patient resource IDs must be client-assigned in patient compartment mode"); Msg.code(1321) + "Patient resource IDs must be client-assigned in patient compartment mode");
} }
} else { } else {
oCompartmentIdentity = oCompartmentIdentity = ResourceCompartmentUtil.getResourceCompartment(
ResourceCompartmentUtil.getResourceCompartment(theResource, compartmentSps, mySearchParamExtractor); "Patient", theResource, compartmentSps, mySearchParamExtractor);
} }
return oCompartmentIdentity return oCompartmentIdentity

View File

@ -45,8 +45,8 @@ public class ResourceCompartmentUtil {
/** /**
* Extract, if exists, the patient compartment identity of the received resource. * Extract, if exists, the patient compartment identity of the received resource.
* It must be invoked in patient compartment mode. * It must be invoked in patient compartment mode.
* @param theResource the resource to which extract the patient compartment identity * @param theResource the resource to which extract the patient compartment identity
* @param theFhirContext the active FhirContext * @param theFhirContext the active FhirContext
* @param theSearchParamExtractor the configured search parameter extractor * @param theSearchParamExtractor the configured search parameter extractor
* @return the optional patient compartment identifier * @return the optional patient compartment identifier
* @throws MethodNotAllowedException if received resource is of type "Patient" and ID is not assigned. * @throws MethodNotAllowedException if received resource is of type "Patient" and ID is not assigned.
@ -69,20 +69,23 @@ public class ResourceCompartmentUtil {
return Optional.of(compartmentIdentity); return Optional.of(compartmentIdentity);
} }
return getResourceCompartment(theResource, patientCompartmentSps, theSearchParamExtractor); return getResourceCompartment("Patient", theResource, patientCompartmentSps, theSearchParamExtractor);
} }
/** /**
* Extracts and returns an optional compartment of the received resource * Extracts and returns an optional compartment of the received resource
* @param theResource source resource which compartment is extracted * @param theCompartmentName the name of the compartment
* @param theCompartmentSps the RuntimeSearchParam list involving the searched compartment * @param theResource source resource which compartment is extracted
* @param theCompartmentSps the RuntimeSearchParam list involving the searched compartment
* @param mySearchParamExtractor the ISearchParamExtractor to be used to extract the parameter values * @param mySearchParamExtractor the ISearchParamExtractor to be used to extract the parameter values
* @return optional compartment of the received resource * @return optional compartment of the received resource
*/ */
public static Optional<String> getResourceCompartment( public static Optional<String> getResourceCompartment(
String theCompartmentName,
IBaseResource theResource, IBaseResource theResource,
List<RuntimeSearchParam> theCompartmentSps, List<RuntimeSearchParam> theCompartmentSps,
ISearchParamExtractor mySearchParamExtractor) { ISearchParamExtractor mySearchParamExtractor) {
// TODO KHS consolidate with FhirTerser.getCompartmentOwnersForResource()
return theCompartmentSps.stream() return theCompartmentSps.stream()
.flatMap(param -> Arrays.stream(BaseSearchParamExtractor.splitPathsR4(param.getPath()))) .flatMap(param -> Arrays.stream(BaseSearchParamExtractor.splitPathsR4(param.getPath())))
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
@ -94,7 +97,10 @@ public class ResourceCompartmentUtil {
.filter(t -> t instanceof IBaseReference) .filter(t -> t instanceof IBaseReference)
.map(t -> (IBaseReference) t) .map(t -> (IBaseReference) t)
.map(t -> t.getReferenceElement().getValue()) .map(t -> t.getReferenceElement().getValue())
.map(t -> new IdType(t).getIdPart()) .map(IdType::new)
.filter(t -> theCompartmentName.equals(
t.getResourceType())) // assume the compartment name matches the resource type
.map(IdType::getIdPart)
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
.findFirst(); .findFirst();
} }

View File

@ -48,7 +48,7 @@ class ResourceCompartmentUtilTest {
when(mySearchParamExtractor.getPathValueExtractor(myResource, "Observation.subject")) when(mySearchParamExtractor.getPathValueExtractor(myResource, "Observation.subject"))
.thenReturn(() -> List.of(new Reference("Patient/P01"))); .thenReturn(() -> List.of(new Reference("Patient/P01")));
Optional<String> oCompartment = ResourceCompartmentUtil.getResourceCompartment( Optional<String> oCompartment = ResourceCompartmentUtil.getResourceCompartment("Patient",
myResource, myCompartmentSearchParams, mySearchParamExtractor); myResource, myCompartmentSearchParams, mySearchParamExtractor);
assertThat(oCompartment).isPresent(); assertThat(oCompartment).isPresent();