Add implementation to validate conditional URLs post-transaction

This commit is contained in:
Tadgh 2021-09-13 13:03:44 -04:00
parent 94f157a4ac
commit 59c8765e6a
4 changed files with 58 additions and 11 deletions

View File

@ -44,7 +44,9 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.parser.DataFormatException;
@ -64,6 +66,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
@ -157,6 +160,8 @@ public abstract class BaseTransactionProcessor {
private ModelConfig myModelConfig;
@Autowired
private InMemoryResourceMatcher myInMemoryResourceMatcher;
@Autowired
private SearchParamMatcher mySearchParamMatcher;
private TaskExecutor myExecutor ;
@ -852,6 +857,7 @@ public abstract class BaseTransactionProcessor {
Map<IBase, IIdType> entriesToProcess = new IdentityHashMap<>();
Set<IIdType> nonUpdatedEntities = new HashSet<>();
Set<IBasePersistedResource> updatedEntities = new HashSet<>();
Map<String, IIdType> conditionalUrlToIdMap = new HashMap<>();
List<IBaseResource> updatedResources = new ArrayList<>();
Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<>();
@ -891,6 +897,7 @@ public abstract class BaseTransactionProcessor {
String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
outcome = resourceDao.create(res, matchUrl, false, theTransactionDetails, theRequest);
conditionalUrlToIdMap.put(matchUrl, outcome.getId());
res.setId(outcome.getId());
if (nextResourceId != null) {
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest);
@ -925,6 +932,7 @@ public abstract class BaseTransactionProcessor {
String matchUrl = parts.getResourceType() + '?' + parts.getParams();
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequest);
conditionalUrlToIdMap.put(matchUrl, deleteOutcome.getId());
List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
for (ResourceTable deleted : allDeleted) {
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString());
@ -967,6 +975,7 @@ public abstract class BaseTransactionProcessor {
}
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
outcome = resourceDao.update(res, matchUrl, false, false, theRequest, theTransactionDetails);
conditionalUrlToIdMap.put(matchUrl, outcome.getId());
if (Boolean.TRUE.equals(outcome.getCreated())) {
conditionalRequestUrls.put(matchUrl, res.getClass());
}
@ -1025,6 +1034,7 @@ public abstract class BaseTransactionProcessor {
IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url);
IIdType patchId = myContext.getVersion().newIdType().setValue(parts.getResourceId());
DaoMethodOutcome outcome = dao.patch(patchId, matchUrl, patchType, patchBody, patchBodyParameters, theRequest);
conditionalUrlToIdMap.put(matchUrl, outcome.getId());
updatedEntities.add(outcome.getEntity());
if (outcome.getResource() != null) {
updatedResources.add(outcome.getResource());
@ -1081,6 +1091,11 @@ public abstract class BaseTransactionProcessor {
if (!myDaoConfig.isMassIngestionMode()) {
validateNoDuplicates(theRequest, theActionName, conditionalRequestUrls, theIdToPersistedOutcome.values());
}
if (!myDaoConfig.isMassIngestionMode()) {
validateAllInsertsMatchTheirConditionalUrls(theIdToPersistedOutcome, conditionalUrlToIdMap, theRequest);
}
theTransactionStopWatch.endCurrentTask();
for (IIdType next : theAllIds) {
@ -1119,6 +1134,32 @@ public abstract class BaseTransactionProcessor {
}
}
/**
* After transaction processing and resolution of indexes and references, we want to validate that the resources that were stored _actually_
* match the conditional URLs that they were brought in on.
* @param theIdToPersistedOutcome
* @param conditionalUrlToIdMap
*/
private void validateAllInsertsMatchTheirConditionalUrls(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, Map<String, IIdType> conditionalUrlToIdMap, RequestDetails theRequest) {
conditionalUrlToIdMap.entrySet().stream()
.forEach(entry -> {
String matchUrl = entry.getKey();
IIdType value = entry.getValue();
DaoMethodOutcome daoMethodOutcome = theIdToPersistedOutcome.get(value);
if (daoMethodOutcome != null && daoMethodOutcome.getResource() != null) {
InMemoryMatchResult match = mySearchParamMatcher.match(matchUrl, daoMethodOutcome.getResource(), theRequest);
if (ourLog.isDebugEnabled()) {
ourLog.debug("Checking conditional URL [{}] against resource with ID [{}]: Supported?:[{}], Matched?:[{}]", matchUrl, value, match.supported(), match.matched());
}
if (match.supported()) {
if (!match.matched()) {
throw new PreconditionFailedException("Invalid conditional URL \"" + matchUrl + "\". The given resource is not matched by this URL.");
};
}
}
});
}
/**
* Checks for any delete conflicts.
* @param theDeleteConflicts - set of delete conflicts
@ -1409,7 +1450,7 @@ public abstract class BaseTransactionProcessor {
thePersistedOutcomes
.stream()
.filter(t -> !t.isNop())
.filter(t -> t.getEntity() instanceof ResourceTable)
.filter(t -> t.getEntity() instanceof ResourceTable)//N.B. GGG: This validation never occurs for mongo, as nothing is a ResourceTable.
.filter(t -> t.getEntity().getDeleted() == null)
.filter(t -> t.getResource() != null)
.forEach(t -> resourceToIndexedParams.put(t.getResource(), new ResourceIndexedSearchParams((ResourceTable) t.getEntity())));

View File

@ -98,7 +98,9 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
@ -1207,12 +1209,14 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testConditionalUrlWhichDoesNotMatcHResource() {
public void testConditionalUrlWhichDoesNotMatchResource() {
Bundle transactionBundle = new Bundle().setType(BundleType.TRANSACTION);
String storedIdentifierValue = "woop";
String conditionalUrlIdentifierValue = "zoop";
// Patient
HumanName patientName = new HumanName().setFamily("TEST_LAST_NAME").addGiven("TEST_FIRST_NAME");
Identifier patientIdentifier = new Identifier().setSystem("http://example.com/mrns").setValue("U1234567890");
Identifier patientIdentifier = new Identifier().setSystem("http://example.com/mrns").setValue(storedIdentifierValue);
Patient patient = new Patient()
.setName(List.of(patientName))
.setIdentifier(List.of(patientIdentifier));
@ -1224,14 +1228,14 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
.setResource(patient)
.getRequest()
.setMethod(Bundle.HTTPVerb.PUT)
.setUrl("/Patient?identifier=" + patientIdentifier.getSystem() + "|" + "zoop");
.setUrl("/Patient?identifier=" + patientIdentifier.getSystem() + "|" + conditionalUrlIdentifierValue);
ourLog.info("Patient TEMP UUID: {}", patient.getId());
String s = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(transactionBundle);
System.out.println(s);
Bundle outcome= mySystemDao.transaction(null, transactionBundle);
String patientLocation = outcome.getEntry().get(0).getResponse().getLocation();
assertThat(patientLocation, matchesPattern("Patient/[a-z0-9-]+/_history/1"));
try {
mySystemDao.transaction(null, transactionBundle);
fail();
} catch (PreconditionFailedException e) {
assertThat(e.getMessage(), is(equalTo("Invalid conditional URL \"Patient?identifier=http://example.com/mrns|" + conditionalUrlIdentifierValue +"\". The given resource is not matched by this URL.")));
}
}
@Test
@ -1267,6 +1271,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testTransactionUpdateTwoResourcesWithSameId() {
Bundle request = new Bundle();

View File

@ -42,7 +42,7 @@
<appender-ref ref="STDOUT" />
</logger>
<logger name="ca.uhn.fhir.jpa.dao" additivity="false" level="info">
<logger name="ca.uhn.fhir.jpa.dao" additivity="false" level="debug">
<appender-ref ref="STDOUT" />
</logger>

View File

@ -156,6 +156,7 @@ public class InMemoryResourceMatcher {
String resourceName = theResourceDefinition.getName();
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
InMemoryMatchResult checkUnsupportedResult = checkUnsupportedPrefixes(theParamName, paramDef, theAndOrParams);
if (!checkUnsupportedResult.supported()) {
return checkUnsupportedResult;
}