3547 Interceptor hook for client-assigned IDs (#3559)
* add pointcut * draft test * fix test, change name and location of pointcut * add changelog * change check condition, improve changelog wording * improve changelog wording * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3547-interceptor-hook-for-client-assigned-ids.yaml Co-authored-by: Tadgh <garygrantgraham@gmail.com> * change pointcut name * update test * fix test * fix changelog * renaming variables in test Co-authored-by: Justin_Dar <justin.dar@smilecdr.com> Co-authored-by: Tadgh <garygrantgraham@gmail.com>
This commit is contained in:
parent
53c8b067d5
commit
5ccb4effb0
|
@ -1345,6 +1345,34 @@ public enum Pointcut implements IPointcut {
|
||||||
"ca.uhn.fhir.rest.api.server.storage.TransactionDetails"
|
"ca.uhn.fhir.rest.api.server.storage.TransactionDetails"
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b>Storage Hook:</b>
|
||||||
|
* Invoked before client-assigned id is created.
|
||||||
|
* <p>
|
||||||
|
* Hooks will have access to the contents of the resource being created
|
||||||
|
* so that client-assigned ids can be allowed/denied. These changes will
|
||||||
|
* be reflected in permanent storage.
|
||||||
|
* </p>
|
||||||
|
* Hooks may accept the following parameters:
|
||||||
|
* <ul>
|
||||||
|
* <li>org.hl7.fhir.instance.model.api.IBaseResource</li>
|
||||||
|
* <li>
|
||||||
|
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
|
||||||
|
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
|
||||||
|
* pulled out of the servlet request. Note that the bean
|
||||||
|
* properties are not all guaranteed to be populated, depending on how early during processing the
|
||||||
|
* exception occurred.
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* Hooks should return <code>void</code>.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
STORAGE_PRESTORAGE_CLIENT_ASSIGNED_ID(void.class,
|
||||||
|
"org.hl7.fhir.instance.model.api.IBaseResource",
|
||||||
|
"ca.uhn.fhir.rest.api.server.RequestDetails"
|
||||||
|
),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <b>Storage Hook:</b>
|
* <b>Storage Hook:</b>
|
||||||
* Invoked before a resource will be updated, immediately before the resource
|
* Invoked before a resource will be updated, immediately before the resource
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
type: add
|
||||||
|
issue: 3547
|
||||||
|
jira: SMILE-4141
|
||||||
|
title: "Added a new pointcut: `STORAGE_PRESTORAGE_CLIENT_ASSIGNED_ID` which is invoked when a user attempts to create a resource with a client-assigned ID. "
|
|
@ -70,6 +70,7 @@ import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||||
import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
|
import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
import ca.uhn.fhir.rest.api.PatchTypeEnum;
|
import ca.uhn.fhir.rest.api.PatchTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
|
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
|
||||||
import ca.uhn.fhir.rest.api.ValidationModeEnum;
|
import ca.uhn.fhir.rest.api.ValidationModeEnum;
|
||||||
|
@ -310,17 +311,28 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails);
|
notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String resourceIdBeforeStorage = theResource.getIdElement().getIdPart();
|
||||||
|
boolean resourceHadIdBeforeStorage = isNotBlank(resourceIdBeforeStorage);
|
||||||
|
boolean resourceIdWasServerAssigned = theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED) == Boolean.TRUE;
|
||||||
|
|
||||||
|
HookParams hookParams;
|
||||||
|
|
||||||
|
// Notify interceptor for accepting/rejecting client assigned ids
|
||||||
|
if (!resourceIdWasServerAssigned && resourceHadIdBeforeStorage) {
|
||||||
|
hookParams = new HookParams()
|
||||||
|
.add(IBaseResource.class, theResource)
|
||||||
|
.add(RequestDetails.class, theRequest);
|
||||||
|
doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_CLIENT_ASSIGNED_ID, hookParams);
|
||||||
|
}
|
||||||
|
|
||||||
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
|
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
|
||||||
HookParams hookParams = new HookParams()
|
hookParams = new HookParams()
|
||||||
.add(IBaseResource.class, theResource)
|
.add(IBaseResource.class, theResource)
|
||||||
.add(RequestDetails.class, theRequest)
|
.add(RequestDetails.class, theRequest)
|
||||||
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
||||||
.add(TransactionDetails.class, theTransactionDetails);
|
.add(TransactionDetails.class, theTransactionDetails);
|
||||||
doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams);
|
doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams);
|
||||||
|
|
||||||
String resourceIdBeforeStorage = theResource.getIdElement().getIdPart();
|
|
||||||
boolean resourceHadIdBeforeStorage = isNotBlank(resourceIdBeforeStorage);
|
|
||||||
boolean resourceIdWasServerAssigned = theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED) == Boolean.TRUE;
|
|
||||||
if (resourceHadIdBeforeStorage && !resourceIdWasServerAssigned) {
|
if (resourceHadIdBeforeStorage && !resourceIdWasServerAssigned) {
|
||||||
validateResourceIdCreation(theResource, theRequest);
|
validateResourceIdCreation(theResource, theRequest);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
|
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
|
||||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
@ -32,6 +33,7 @@ import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.inOrder;
|
import static org.mockito.Mockito.inOrder;
|
||||||
|
@ -511,6 +513,7 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.setActive(false);
|
p.setActive(false);
|
||||||
|
|
||||||
IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
|
IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
p = myPatientDao.read(id);
|
p = myPatientDao.read(id);
|
||||||
|
@ -518,6 +521,37 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPrestorageClientAssignedIdInterceptorCanDenyClientAssignedIds() {
|
||||||
|
Object interceptor = new Object() {
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_CLIENT_ASSIGNED_ID)
|
||||||
|
public void prestorageClientAssignedId(IBaseResource theResource, RequestDetails theRequest) {
|
||||||
|
throw new ForbiddenOperationException("Client assigned id rejected.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mySrdInterceptorService.registerInterceptor(interceptor);
|
||||||
|
|
||||||
|
{//Ensure interceptor is not invoked on create.
|
||||||
|
Patient serverAssignedPatient = new Patient();
|
||||||
|
try {
|
||||||
|
myPatientDao.create(serverAssignedPatient, mySrd);
|
||||||
|
} catch (ForbiddenOperationException e) {
|
||||||
|
fail("Interceptor was invoked, and should not have been!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{//Ensure attempting to set a client assigned id is rejected by our interceptor.
|
||||||
|
try {
|
||||||
|
Patient clientAssignedPatient = new Patient();
|
||||||
|
clientAssignedPatient.setId("Patient/custom-id");
|
||||||
|
myPatientDao.update(clientAssignedPatient, mySrd);
|
||||||
|
fail();
|
||||||
|
} catch (ForbiddenOperationException e) {
|
||||||
|
assertEquals("Client assigned id rejected.", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure that both JPA interceptors and RestfulServer interceptors can
|
* Make sure that both JPA interceptors and RestfulServer interceptors can
|
||||||
* get called
|
* get called
|
||||||
|
|
Loading…
Reference in New Issue