Add BasicAuditEventLogging (BALP) Interceptor (#4787)
* Begin work on BALP interceptor * Work on BALP * Basic profile * Work on BALP * Add BALP * Work on balp * Add update and delete * Work on BALP * Add logging * Tests * Modify test server * Work on docs * Add documentation * Add changelog * Fix #4728 - Typos in docs * Test fixes * Move changelog * Update hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/balp/BalpAuditCaptureInterceptor.java Co-authored-by: michaelabuckley <michaelabuckley@gmail.com> * Resolve PR comments * Test fixes --------- Co-authored-by: michaelabuckley <michaelabuckley@gmail.com>
This commit is contained in:
parent
f4c0f6e9fc
commit
a3c33d2a53
|
@ -760,6 +760,76 @@ public class FhirTerser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CompartmentOwnerVisitor implements ICompartmentOwnerVisitor {
|
||||||
|
|
||||||
|
private final String myWantRef;
|
||||||
|
|
||||||
|
public boolean isFound() {
|
||||||
|
return myFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean myFound;
|
||||||
|
|
||||||
|
public CompartmentOwnerVisitor(String theWantRef) {
|
||||||
|
myWantRef = theWantRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean consume(IIdType theCompartmentOwner) {
|
||||||
|
if (myWantRef.equals(theCompartmentOwner.toUnqualifiedVersionless().getValue())) {
|
||||||
|
myFound = true;
|
||||||
|
}
|
||||||
|
return !myFound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompartmentOwnerVisitor consumer = new CompartmentOwnerVisitor(wantRef);
|
||||||
|
visitCompartmentOwnersForResource(theCompartmentName, theSource, theAdditionalCompartmentParamNames, consumer);
|
||||||
|
return consumer.isFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the owners of the compartment in <code>theSource</code> is in the compartment named <code>theCompartmentName</code>.
|
||||||
|
*
|
||||||
|
* @param theCompartmentName The name of the compartment
|
||||||
|
* @param theSource The potential member of the compartment
|
||||||
|
* @param theAdditionalCompartmentParamNames If provided, search param names provided here will be considered as included in the given compartment for this comparison.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public List<IIdType> getCompartmentOwnersForResource(String theCompartmentName, IBaseResource theSource, Set<String> theAdditionalCompartmentParamNames) {
|
||||||
|
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank");
|
||||||
|
Validate.notNull(theSource, "theSource must not be null");
|
||||||
|
|
||||||
|
class CompartmentOwnerVisitor implements ICompartmentOwnerVisitor {
|
||||||
|
|
||||||
|
private final Set<String> myOwnersAdded = new HashSet<>();
|
||||||
|
private final List<IIdType> myOwners = new ArrayList<>(2);
|
||||||
|
|
||||||
|
public List<IIdType> getOwners() {
|
||||||
|
return myOwners;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean consume(IIdType theCompartmentOwner) {
|
||||||
|
if (myOwnersAdded.add(theCompartmentOwner.getValue())) {
|
||||||
|
myOwners.add(theCompartmentOwner);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompartmentOwnerVisitor consumer = new CompartmentOwnerVisitor();
|
||||||
|
visitCompartmentOwnersForResource(theCompartmentName, theSource, theAdditionalCompartmentParamNames, consumer);
|
||||||
|
return consumer.getOwners();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void visitCompartmentOwnersForResource(String theCompartmentName, IBaseResource theSource, Set<String> theAdditionalCompartmentParamNames, ICompartmentOwnerVisitor theConsumer) {
|
||||||
|
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank");
|
||||||
|
Validate.notNull(theSource, "theSource must not be null");
|
||||||
|
|
||||||
|
RuntimeResourceDefinition sourceDef = myContext.getResourceDefinition(theSource);
|
||||||
List<RuntimeSearchParam> params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName);
|
List<RuntimeSearchParam> params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName);
|
||||||
|
|
||||||
// If passed an additional set of searchparameter names, add them for comparison purposes.
|
// If passed an additional set of searchparameter names, add them for comparison purposes.
|
||||||
|
@ -777,7 +847,6 @@ public class FhirTerser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (RuntimeSearchParam nextParam : params) {
|
for (RuntimeSearchParam nextParam : params) {
|
||||||
for (String nextPath : nextParam.getPathsSplit()) {
|
for (String nextPath : nextParam.getPathsSplit()) {
|
||||||
|
|
||||||
|
@ -799,22 +868,20 @@ public class FhirTerser {
|
||||||
|
|
||||||
List<IBaseReference> values = getValues(theSource, nextPath, IBaseReference.class);
|
List<IBaseReference> values = getValues(theSource, nextPath, IBaseReference.class);
|
||||||
for (IBaseReference nextValue : values) {
|
for (IBaseReference nextValue : values) {
|
||||||
IIdType nextTargetId = nextValue.getReferenceElement();
|
IIdType nextTargetId = nextValue.getReferenceElement().toUnqualifiedVersionless();
|
||||||
String nextRef = nextTargetId.toUnqualifiedVersionless().getValue();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If the reference isn't an explicit resource ID, but instead is just
|
* If the reference isn't an explicit resource ID, but instead is just
|
||||||
* a resource object, we'll calculate its ID and treat the target
|
* a resource object, we'll calculate its ID and treat the target
|
||||||
* as that.
|
* as that.
|
||||||
*/
|
*/
|
||||||
if (isBlank(nextRef) && nextValue.getResource() != null) {
|
if (isBlank(nextTargetId.getValue()) && nextValue.getResource() != null) {
|
||||||
IBaseResource nextTarget = nextValue.getResource();
|
IBaseResource nextTarget = nextValue.getResource();
|
||||||
nextTargetId = nextTarget.getIdElement().toUnqualifiedVersionless();
|
nextTargetId = nextTarget.getIdElement().toUnqualifiedVersionless();
|
||||||
if (!nextTargetId.hasResourceType()) {
|
if (!nextTargetId.hasResourceType()) {
|
||||||
String resourceType = myContext.getResourceType(nextTarget);
|
String resourceType = myContext.getResourceType(nextTarget);
|
||||||
nextTargetId.setParts(null, resourceType, nextTargetId.getIdPart(), null);
|
nextTargetId.setParts(null, resourceType, nextTargetId.getIdPart(), null);
|
||||||
}
|
}
|
||||||
nextRef = nextTargetId.getValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNotBlank(wantType)) {
|
if (isNotBlank(wantType)) {
|
||||||
|
@ -824,14 +891,18 @@ public class FhirTerser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantRef.equals(nextRef)) {
|
if (isNotBlank(nextTargetId.getValue())) {
|
||||||
return true;
|
boolean shouldContinue = theConsumer.consume(nextTargetId);
|
||||||
|
if (!shouldContinue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void visit(IBase theElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition, IModelVisitor2 theCallback, List<IBase> theContainingElementPath,
|
private void visit(IBase theElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition, IModelVisitor2 theCallback, List<IBase> theContainingElementPath,
|
||||||
|
@ -1376,7 +1447,6 @@ public class FhirTerser {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds and returns a new element at the given path within the given structure. The paths used here
|
* Adds and returns a new element at the given path within the given structure. The paths used here
|
||||||
* are <b>not FHIRPath expressions</b> but instead just simple dot-separated path expressions.
|
* are <b>not FHIRPath expressions</b> but instead just simple dot-separated path expressions.
|
||||||
|
@ -1407,7 +1477,6 @@ public class FhirTerser {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method has the same semantics as {@link #addElement(IBase, String, String)} but adds
|
* This method has the same semantics as {@link #addElement(IBase, String, String)} but adds
|
||||||
* a collection of primitives instead of a single one.
|
* a collection of primitives instead of a single one.
|
||||||
|
@ -1452,7 +1521,6 @@ public class FhirTerser {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public enum OptionsEnum {
|
public enum OptionsEnum {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1468,6 +1536,17 @@ public class FhirTerser {
|
||||||
STORE_AND_REUSE_RESULTS
|
STORE_AND_REUSE_RESULTS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface ICompartmentOwnerVisitor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns true if we should keep looking for more
|
||||||
|
*/
|
||||||
|
boolean consume(IIdType theCompartmentOwner);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public static class ContainedResources {
|
public static class ContainedResources {
|
||||||
private long myNextContainedId = 1;
|
private long myNextContainedId = 1;
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseDatatype;
|
import org.hl7.fhir.instance.model.api.IBaseDatatype;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseParameters;
|
import org.hl7.fhir.instance.model.api.IBaseParameters;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.r4.model.AuditEvent;
|
||||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
import org.hl7.fhir.r4.model.CodeSystem;
|
import org.hl7.fhir.r4.model.CodeSystem;
|
||||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||||
|
@ -271,6 +272,10 @@ public class VersionCanonicalizer {
|
||||||
return myStrategy.codeSystemToValidatorCanonical(theResource);
|
return myStrategy.codeSystemToValidatorCanonical(theResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IBaseResource auditEventFromCanonical(AuditEvent theResource) {
|
||||||
|
return myStrategy.auditEventFromCanonical(theResource);
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private List<String> extractNonStandardSearchParameterListAndClearSourceIfAnyArePresent(IBaseResource theSearchParameter, String theChildName) {
|
private List<String> extractNonStandardSearchParameterListAndClearSourceIfAnyArePresent(IBaseResource theSearchParameter, String theChildName) {
|
||||||
|
|
||||||
|
@ -326,6 +331,10 @@ public class VersionCanonicalizer {
|
||||||
org.hl7.fhir.r5.model.CodeSystem codeSystemToValidatorCanonical(IBaseResource theResource);
|
org.hl7.fhir.r5.model.CodeSystem codeSystemToValidatorCanonical(IBaseResource theResource);
|
||||||
|
|
||||||
IBaseResource searchParameterFromCanonical(SearchParameter theResource);
|
IBaseResource searchParameterFromCanonical(SearchParameter theResource);
|
||||||
|
|
||||||
|
|
||||||
|
IBaseResource auditEventFromCanonical(AuditEvent theResource);
|
||||||
|
|
||||||
|
|
||||||
IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource);
|
IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource);
|
||||||
}
|
}
|
||||||
|
@ -478,6 +487,12 @@ public class VersionCanonicalizer {
|
||||||
return (org.hl7.fhir.r5.model.CodeSystem) VersionConvertorFactory_10_50.convertResource(reencoded, ADVISOR_10_50);
|
return (org.hl7.fhir.r5.model.CodeSystem) VersionConvertorFactory_10_50.convertResource(reencoded, ADVISOR_10_50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBaseResource auditEventFromCanonical(AuditEvent theResource) {
|
||||||
|
Resource hl7Org = VersionConvertorFactory_10_40.convertResource(theResource, ADVISOR_10_40);
|
||||||
|
return reencodeFromHl7Org(hl7Org);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBaseResource searchParameterFromCanonical(SearchParameter theResource) {
|
public IBaseResource searchParameterFromCanonical(SearchParameter theResource) {
|
||||||
Resource resource = VersionConvertorFactory_10_50.convertResource(theResource, ADVISOR_10_50);
|
Resource resource = VersionConvertorFactory_10_50.convertResource(theResource, ADVISOR_10_50);
|
||||||
|
@ -588,6 +603,11 @@ public class VersionCanonicalizer {
|
||||||
return VersionConvertorFactory_14_50.convertResource(theResource, ADVISOR_14_50);
|
return VersionConvertorFactory_14_50.convertResource(theResource, ADVISOR_14_50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBaseResource auditEventFromCanonical(AuditEvent theResource) {
|
||||||
|
return VersionConvertorFactory_14_40.convertResource(theResource, ADVISOR_14_40);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
||||||
return (IBaseConformance) VersionConvertorFactory_14_50.convertResource(theResource, ADVISOR_14_50);
|
return (IBaseConformance) VersionConvertorFactory_14_50.convertResource(theResource, ADVISOR_14_50);
|
||||||
|
@ -676,6 +696,11 @@ public class VersionCanonicalizer {
|
||||||
return VersionConvertorFactory_30_50.convertResource(theResource, ADVISOR_30_50);
|
return VersionConvertorFactory_30_50.convertResource(theResource, ADVISOR_30_50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBaseResource auditEventFromCanonical(AuditEvent theResource) {
|
||||||
|
return VersionConvertorFactory_30_40.convertResource(theResource, ADVISOR_30_40);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
||||||
return (IBaseConformance) VersionConvertorFactory_30_50.convertResource(theResource, ADVISOR_30_50);
|
return (IBaseConformance) VersionConvertorFactory_30_50.convertResource(theResource, ADVISOR_30_50);
|
||||||
|
@ -763,6 +788,11 @@ public class VersionCanonicalizer {
|
||||||
return VersionConvertorFactory_40_50.convertResource(theResource, ADVISOR_40_50);
|
return VersionConvertorFactory_40_50.convertResource(theResource, ADVISOR_40_50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBaseResource auditEventFromCanonical(AuditEvent theResource) {
|
||||||
|
return theResource;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
||||||
return (IBaseConformance) VersionConvertorFactory_40_50.convertResource(theResource, ADVISOR_40_50);
|
return (IBaseConformance) VersionConvertorFactory_40_50.convertResource(theResource, ADVISOR_40_50);
|
||||||
|
@ -859,6 +889,12 @@ public class VersionCanonicalizer {
|
||||||
return VersionConvertorFactory_43_50.convertResource(theResource, ADVISOR_43_50);
|
return VersionConvertorFactory_43_50.convertResource(theResource, ADVISOR_43_50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBaseResource auditEventFromCanonical(AuditEvent theResource) {
|
||||||
|
org.hl7.fhir.r5.model.AuditEvent r5 = (org.hl7.fhir.r5.model.AuditEvent) VersionConvertorFactory_40_50.convertResource(theResource, ADVISOR_40_50);
|
||||||
|
return VersionConvertorFactory_43_50.convertResource(r5, ADVISOR_43_50);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
||||||
return (IBaseConformance) VersionConvertorFactory_43_50.convertResource(theResource, ADVISOR_43_50);
|
return (IBaseConformance) VersionConvertorFactory_43_50.convertResource(theResource, ADVISOR_43_50);
|
||||||
|
@ -948,6 +984,11 @@ public class VersionCanonicalizer {
|
||||||
return theResource;
|
return theResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBaseResource auditEventFromCanonical(AuditEvent theResource) {
|
||||||
|
return VersionConvertorFactory_40_50.convertResource(theResource, ADVISOR_40_50);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
public IBaseConformance capabilityStatementFromCanonical(CapabilityStatement theResource) {
|
||||||
return theResource;
|
return theResource;
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
package ca.uhn.hapi.fhir.docs;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.AsyncMemoryQueueBackedFhirClientBalpSink;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.IBalpAuditContextServices;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.IBalpAuditEventSink;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor;
|
||||||
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BalpExample {
|
||||||
|
|
||||||
|
//START SNIPPET: contextService
|
||||||
|
public class ExampleBalpAuditContextServices implements IBalpAuditContextServices {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we are just hard-coding a simple display name. In a real implementation
|
||||||
|
* we should use the actual identity of the requesting client.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Reference getAgentClientWho(RequestDetails theRequestDetails) {
|
||||||
|
Reference client = new Reference();
|
||||||
|
client.setDisplay("Growth Chart Application");
|
||||||
|
client.getIdentifier()
|
||||||
|
.setSystem("http://example.org/clients")
|
||||||
|
.setValue("growth_chart");
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we are just hard-coding a simple display name. In a real implementation
|
||||||
|
* we should use the actual identity of the requesting user.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Reference getAgentUserWho(RequestDetails theRequestDetails) {
|
||||||
|
Reference user = new Reference();
|
||||||
|
user.getIdentifier()
|
||||||
|
.setSystem("http://example.org/users")
|
||||||
|
.setValue("my_username");
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//END SNIPPET: contextService
|
||||||
|
|
||||||
|
//START SNIPPET: server
|
||||||
|
public class MyServer extends RestfulServer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public MyServer() {
|
||||||
|
super(FhirContext.forR4Cached());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize() throws ServletException {
|
||||||
|
// Register your resource providers and other interceptors here...
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create our context sservices object
|
||||||
|
*/
|
||||||
|
IBalpAuditContextServices contextServices = new ExampleBalpAuditContextServices();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create our event sink
|
||||||
|
*/
|
||||||
|
FhirContext fhirContext = FhirContext.forR4Cached();
|
||||||
|
String targetUrl = "http://my.fhir.server/baseR4";
|
||||||
|
List<Object> clientInterceptors = List.of(
|
||||||
|
// We'll register an auth interceptor against the sink FHIR client so that
|
||||||
|
// credentials get passed to the target server. Of course in a real implementation
|
||||||
|
// you should never hard code credentials like this.
|
||||||
|
new BasicAuthInterceptor("username", "password")
|
||||||
|
);
|
||||||
|
IBalpAuditEventSink eventSink = new AsyncMemoryQueueBackedFhirClientBalpSink(fhirContext, targetUrl, clientInterceptors);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//END SNIPPET: server
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 4787
|
||||||
|
title: "A new interceptor called BalpAuditCaptureInterceptor has been added. This interceptor
|
||||||
|
automatically generates AuditEvent resources in a HAPI FHIR server that are conformant with the
|
||||||
|
[IHE BasicAudit Log Patterns](https://profiles.ihe.net/ITI/BALP/) IG."
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
type: fix
|
||||||
|
issue: 4787
|
||||||
|
title: "HashMapResourceProvider invoked the `STORAGE_PRESTORAGE_xxx` and
|
||||||
|
`STORAGE_PRECOMMIT_xxx` pointcuts after storing the resource into memory. This
|
||||||
|
means that if any interceptors threw an exception in an attempt to abort the
|
||||||
|
transaction, this did not actually result in the resource storage being aborted.
|
||||||
|
This has been corrected."
|
|
@ -105,6 +105,7 @@ page.security.authorization_interceptor=Authorization Interceptor
|
||||||
page.security.consent_interceptor=Consent Interceptor
|
page.security.consent_interceptor=Consent Interceptor
|
||||||
page.security.search_narrowing_interceptor=Search Narrowing Interceptor
|
page.security.search_narrowing_interceptor=Search Narrowing Interceptor
|
||||||
page.security.cors=CORS
|
page.security.cors=CORS
|
||||||
|
page.security.balp_interceptor=Basic Audit Log Pattern (BALP)
|
||||||
|
|
||||||
section.validation.title=Validation
|
section.validation.title=Validation
|
||||||
page.validation.introduction=Introduction
|
page.validation.introduction=Introduction
|
||||||
|
|
|
@ -173,10 +173,14 @@ The following example shows how to register this interceptor within a HAPI FHIR
|
||||||
|
|
||||||
HAPI FHIR includes an interceptor which can be used to implement CORS support on your server. See [Server CORS Documentation](/docs/security/cors.html#cors_interceptor) for information on how to use this interceptor.
|
HAPI FHIR includes an interceptor which can be used to implement CORS support on your server. See [Server CORS Documentation](/docs/security/cors.html#cors_interceptor) for information on how to use this interceptor.
|
||||||
|
|
||||||
|
# Security: Audit
|
||||||
|
|
||||||
|
HAPI FHIR provides an interceptor that can be used to automatically generate and record AuditEvent resources based on user/client actions on the server. See [BALP Interceptor](../security/balp_interceptor.html) for more information.
|
||||||
|
|
||||||
|
|
||||||
# Security: Authorization
|
# Security: Authorization
|
||||||
|
|
||||||
HAPI FHIR provides a powerful interceptor that can be used to implement user- and system-level authorization rules that are aware of FHIR semantics. See [Authorization](/docs/security/authorization_interceptor.html) for more information.
|
HAPI FHIR provides an interceptor that can be used to implement user- and system-level authorization rules that are aware of FHIR semantics. See [Authorization](/docs/security/authorization_interceptor.html) for more information.
|
||||||
|
|
||||||
|
|
||||||
# Security: Consent
|
# Security: Consent
|
||||||
|
|
|
@ -100,7 +100,7 @@ Finally, use the [CustomThymeleafNarrativeGenerator](/hapi-fhir/apidocs/hapi-fhi
|
||||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/NarrativeGenerator.java|gen}}
|
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/NarrativeGenerator.java|gen}}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Fragments Expressions in Thyemleaf Templates
|
# Fragments Expressions in Thymeleaf Templates
|
||||||
|
|
||||||
Thymeleaf has a concept called Fragments, which allow reusable template portions that can be imported anywhere you need them. It can be helpful to put these fragment definitions in their own file. For example, the following property file declares a template and a fragment:
|
Thymeleaf has a concept called Fragments, which allow reusable template portions that can be imported anywhere you need them. It can be helpful to put these fragment definitions in their own file. For example, the following property file declares a template and a fragment:
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ And the following parent template (`narrative-with-fragment-parent.html`) import
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# FHIRPath Expressions in Thyemleaf Templates
|
# FHIRPath Expressions in Thymeleaf Templates
|
||||||
|
|
||||||
Thymeleaf templates can incorporate FHIRPath expressions using the `#fhirpath` expression object.
|
Thymeleaf templates can incorporate FHIRPath expressions using the `#fhirpath` expression object.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
# Basic Audit Log Patterns (BALP) Interceptor
|
||||||
|
|
||||||
|
The IHE [Basic Audit Log Patterns](https://profiles.ihe.net/ITI/BALP/) implementation guide describes a set of workflows and data models for the creation of [AuditEvent](http://hl7.org/fhir/AuditEvent.html) resources based on user/client actions.
|
||||||
|
|
||||||
|
HAPI FHIR provides an interceptor that can be registered against a server, and will observe events on that server and automatically generate AuditEvent resources which are conformant to the appropriate profiles within the BALP specification.
|
||||||
|
|
||||||
|
This interceptor implements the following profiles:
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>BALP Profile</th>
|
||||||
|
<th>Trigger</th>
|
||||||
|
<th>Triggering Pointcut</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.Create.html">Create</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a resource has been created that is not a member of the Patient compartment.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRECOMMIT_RESOURCE_CREATED">STORAGE_PRECOMMIT_RESOURCE_CREATED</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.PatientCreate.html">PatientCreate</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a resource has been created that is a member of the Patient compartment.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRECOMMIT_RESOURCE_CREATED">STORAGE_PRECOMMIT_RESOURCE_CREATED</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.Read.html">Read</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a resource has been read that is not a member of the Patient compartment.
|
||||||
|
Note that
|
||||||
|
other extended operations which expose individual resource data may also trigger the creation of
|
||||||
|
an AuditEvent with this profile. For example, the <code>$diff</code> operation exposes data within
|
||||||
|
a resource, so it will also trigger this event.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRESHOW_RESOURCES">STORAGE_PRESHOW_RESOURCES</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.PatientRead.html">PatientRead</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a resource has been read that is a member of the Patient compartment.
|
||||||
|
Note that
|
||||||
|
other extended operations which expose individual resource data may also trigger the creation of
|
||||||
|
an AuditEvent with this profile. For example, the <code>$diff</code> operation exposes data within
|
||||||
|
a resource, so it will also trigger this event.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRESHOW_RESOURCES">STORAGE_PRESHOW_RESOURCES</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.Update.html">Update</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a resource has been updated that is not a member of the Patient compartment.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRECOMMIT_RESOURCE_UPDATED">STORAGE_PRECOMMIT_RESOURCE_UPDATED</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.PatientUpdate.html">PatientUpdate</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a resource has been updated that is a member of the Patient compartment.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRECOMMIT_RESOURCE_UPDATED">STORAGE_PRECOMMIT_RESOURCE_UPDATED</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.Delete.html">Delete</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a resource has been deleted that is not a member of the Patient compartment.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRECOMMIT_RESOURCE_DELETED">STORAGE_PRECOMMIT_RESOURCE_DELETED</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.PatientDelete.html">PatientDelete</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a resource has been deleted that is a member of the Patient compartment.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRECOMMIT_RESOURCE_DELETED">STORAGE_PRECOMMIT_RESOURCE_DELETED</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.Query.html">Query</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a non-patient-oriented search is performed. This refers to a search that is returning
|
||||||
|
data that is not in a specific patient compartment.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRESHOW_RESOURCES">STORAGE_PRESHOW_RESOURCES</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="https://profiles.ihe.net/ITI/BALP/StructureDefinition-IHE.BasicAudit.PatientQuery.html">PatientQuery</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Performed when a patient-oriented search is performed. This refers to a search that returns data in
|
||||||
|
a specific patient compartment.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PRESHOW_RESOURCES">STORAGE_PRESHOW_RESOURCES</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
The HAPI FHIR BALP infrastructure consists of the following components:
|
||||||
|
|
||||||
|
* The [BalpAuditCaptureInterceptor](https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/balp/BalpAuditCaptureInterceptor.html) is the primary interceptor, which you register against a HAPI FHIR [Plain Server](../server_plain/).
|
||||||
|
* The [IBalpAuditEventSink](https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/balp/IBalpAuditEventSink.html) is an interface which receives generated AuditEvents and processes them. Appropriate processing will depend on your use case, but could be storing them locally, transmitting them to a remote server, logging them to a syslog, or even selectively dropping them. See [Audit Event Sink](#audit-event-sink) below.
|
||||||
|
* The [IBalpAuditContextServices](https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/balp/IBalpAuditContextServices.html) is an interface which supplies context information for a given client action. When generating a BALP conformant AuditEvent resource, the BalpAuditCaptureInterceptor will automatically populate most of the AuditEvent with details such as the _entity_ (ie. the resource being accessed or modified) and the _server_ (the FHIR server being used to transmit or store the information). However, other information such as the agent and the user (ie. the FHIR client and the physical user) are not known to HAPI FHIR and must be supplied for each request. This interface supplies these details.
|
||||||
|
|
||||||
|
<a name="audit-event-sink"/>
|
||||||
|
|
||||||
|
# Audit Event Sink
|
||||||
|
|
||||||
|
The BALP [IBalpAuditEventSink](https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/balp/IBalpAuditEventSink.html) receives and handles generated audit events.
|
||||||
|
|
||||||
|
This interface is designed to support custom implementations, so you can absolutely create your own. HAPI FHIR ships with the following implementation:
|
||||||
|
|
||||||
|
* [AsyncMemoryQueueBackedFhirClientBalpSink](https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/balp/AsyncMemoryQueueBackedFhirClientBalpSink.html) uses an HTTP/REST FHIR client to transmit AuditEvents to a FHIR server endpoint. This can be a local or a remote endpoint, and can be a server with any version of FHIR. Messages are transmitted asynchronously using an in-memory queue.
|
||||||
|
|
||||||
|
If you create an implementation of this interface that you think would be useful to others, we would welcome community contributions!
|
||||||
|
|
||||||
|
<a name="audit-context-services"/>
|
||||||
|
|
||||||
|
# Audit Context Services
|
||||||
|
|
||||||
|
In order to use this interceptor, you must suply an instance of [IBalpAuditContextServices](https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/balp/IBalpAuditContextServices.html). This interface supplies the information about each request that the interceptor cannot determine on its own, such as the identity of the requesting user and the requesting client.
|
||||||
|
|
||||||
|
The implementation of this interface for the [public HAPI FHIR server](https://hapi.fhir.org) is available [here](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/FhirTestBalpAuditContextServices.java).
|
||||||
|
|
||||||
|
# Example
|
||||||
|
|
||||||
|
The following example shows a simple implementation of the Context Services:
|
||||||
|
|
||||||
|
```java
|
||||||
|
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/BalpExample.java|contextService}}
|
||||||
|
```
|
||||||
|
|
||||||
|
And the following example shows a HAPI FHIR Basic Server with the BALP interceptor wired in:
|
||||||
|
|
||||||
|
```java
|
||||||
|
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/BalpExample.java|server}}
|
||||||
|
```
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.interceptor;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
|
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
@ -13,6 +14,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.empty;
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.emptyOrNullString;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
@ -153,7 +156,32 @@ public class ForceOffsetSearchModeInterceptorTest extends BaseResourceProviderR4
|
||||||
assertEquals(1, myCaptureQueriesListener.countCommits());
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
assertThat(outcome.getLink("next").getUrl(), containsString("Patient?_count=7&_offset=7&active=true"));
|
assertThat(outcome.getLink(Constants.LINK_NEXT).getUrl(), containsString("Patient?_count=7&_offset=7&active=true"));
|
||||||
|
assertNull(outcome.getLink(Constants.LINK_PREVIOUS));
|
||||||
|
|
||||||
|
// Second page
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
outcome = myClient
|
||||||
|
.loadPage()
|
||||||
|
.next(outcome)
|
||||||
|
.execute();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(outcome).toString(), toUnqualifiedVersionlessIdValues(outcome), containsInAnyOrder(
|
||||||
|
"Patient/A7", "Patient/A8", "Patient/A9"
|
||||||
|
));
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("limit '7'"));
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
|
assertThat(outcome.getLink(Constants.LINK_NEXT).getUrl(), containsString("Patient?_count=7&_offset=14&active=true"));
|
||||||
|
assertThat(outcome.getLink(Constants.LINK_PREVIOUS).getUrl(), containsString("Patient?_count=7&_offset=0&active=true"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider;
|
||||||
import ca.uhn.fhir.jpa.delete.ThreadSafeResourceDeleterSvc;
|
import ca.uhn.fhir.jpa.delete.ThreadSafeResourceDeleterSvc;
|
||||||
import ca.uhn.fhir.jpa.graphql.GraphQLProvider;
|
import ca.uhn.fhir.jpa.graphql.GraphQLProvider;
|
||||||
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.BalpAuditCaptureInterceptor;
|
||||||
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider;
|
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider;
|
||||||
import ca.uhn.fhir.jpa.provider.DiffProvider;
|
import ca.uhn.fhir.jpa.provider.DiffProvider;
|
||||||
import ca.uhn.fhir.jpa.provider.JpaCapabilityStatementProvider;
|
import ca.uhn.fhir.jpa.provider.JpaCapabilityStatementProvider;
|
||||||
|
@ -38,6 +39,7 @@ import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory;
|
import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory;
|
||||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||||
import ca.uhn.fhirtest.config.SqlCaptureInterceptor;
|
import ca.uhn.fhirtest.config.SqlCaptureInterceptor;
|
||||||
|
import ca.uhn.fhirtest.config.TestAuditConfig;
|
||||||
import ca.uhn.fhirtest.config.TestDstu2Config;
|
import ca.uhn.fhirtest.config.TestDstu2Config;
|
||||||
import ca.uhn.fhirtest.config.TestDstu3Config;
|
import ca.uhn.fhirtest.config.TestDstu3Config;
|
||||||
import ca.uhn.fhirtest.config.TestR4BConfig;
|
import ca.uhn.fhirtest.config.TestR4BConfig;
|
||||||
|
@ -59,6 +61,7 @@ import java.util.List;
|
||||||
public class TestRestfulServer extends RestfulServer {
|
public class TestRestfulServer extends RestfulServer {
|
||||||
|
|
||||||
public static final String FHIR_BASEURL_R5 = "fhir.baseurl.r5";
|
public static final String FHIR_BASEURL_R5 = "fhir.baseurl.r5";
|
||||||
|
public static final String FHIR_BASEURL_AUDIT = "fhir.baseurl.audit";
|
||||||
public static final String FHIR_BASEURL_R4 = "fhir.baseurl.r4";
|
public static final String FHIR_BASEURL_R4 = "fhir.baseurl.r4";
|
||||||
public static final String FHIR_BASEURL_R4B = "fhir.baseurl.r4b";
|
public static final String FHIR_BASEURL_R4B = "fhir.baseurl.r4b";
|
||||||
public static final String FHIR_BASEURL_DSTU2 = "fhir.baseurl.dstu2";
|
public static final String FHIR_BASEURL_DSTU2 = "fhir.baseurl.dstu2";
|
||||||
|
@ -113,12 +116,13 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
myAppCtx.register(TestDstu2Config.class, WebsocketDispatcherConfig.class);
|
myAppCtx.register(TestDstu2Config.class, WebsocketDispatcherConfig.class);
|
||||||
baseUrlProperty = FHIR_BASEURL_DSTU2;
|
baseUrlProperty = FHIR_BASEURL_DSTU2;
|
||||||
myAppCtx.refresh();
|
myAppCtx.refresh();
|
||||||
setFhirContext(FhirContext.forDstu2Cached());
|
setFhirContext(myAppCtx.getBean(FhirContext.class));
|
||||||
beans = myAppCtx.getBean("myResourceProvidersDstu2", ResourceProviderFactory.class);
|
beans = myAppCtx.getBean("myResourceProvidersDstu2", ResourceProviderFactory.class);
|
||||||
systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class);
|
systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class);
|
||||||
etagSupport = ETagSupportEnum.ENABLED;
|
etagSupport = ETagSupportEnum.ENABLED;
|
||||||
JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, myAppCtx.getBean(JpaStorageSettings.class));
|
JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, myAppCtx.getBean(JpaStorageSettings.class));
|
||||||
setServerConformanceProvider(confProvider);
|
setServerConformanceProvider(confProvider);
|
||||||
|
registerInterceptor(myAppCtx.getBean(BalpAuditCaptureInterceptor.class));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "DSTU3": {
|
case "DSTU3": {
|
||||||
|
@ -128,7 +132,7 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
myAppCtx.register(TestDstu3Config.class, WebsocketDispatcherConfig.class);
|
myAppCtx.register(TestDstu3Config.class, WebsocketDispatcherConfig.class);
|
||||||
baseUrlProperty = FHIR_BASEURL_DSTU3;
|
baseUrlProperty = FHIR_BASEURL_DSTU3;
|
||||||
myAppCtx.refresh();
|
myAppCtx.refresh();
|
||||||
setFhirContext(FhirContext.forDstu3Cached());
|
setFhirContext(myAppCtx.getBean(FhirContext.class));
|
||||||
beans = myAppCtx.getBean("myResourceProvidersDstu3", ResourceProviderFactory.class);
|
beans = myAppCtx.getBean("myResourceProvidersDstu3", ResourceProviderFactory.class);
|
||||||
systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class);
|
systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class);
|
||||||
etagSupport = ETagSupportEnum.ENABLED;
|
etagSupport = ETagSupportEnum.ENABLED;
|
||||||
|
@ -136,6 +140,7 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
setServerConformanceProvider(confProvider);
|
setServerConformanceProvider(confProvider);
|
||||||
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
|
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
|
||||||
providers.add(myAppCtx.getBean(GraphQLProvider.class));
|
providers.add(myAppCtx.getBean(GraphQLProvider.class));
|
||||||
|
registerInterceptor(myAppCtx.getBean(BalpAuditCaptureInterceptor.class));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "R4": {
|
case "R4": {
|
||||||
|
@ -145,7 +150,7 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
myAppCtx.register(TestR4Config.class, WebsocketDispatcherConfig.class);
|
myAppCtx.register(TestR4Config.class, WebsocketDispatcherConfig.class);
|
||||||
baseUrlProperty = FHIR_BASEURL_R4;
|
baseUrlProperty = FHIR_BASEURL_R4;
|
||||||
myAppCtx.refresh();
|
myAppCtx.refresh();
|
||||||
setFhirContext(FhirContext.forR4Cached());
|
setFhirContext(myAppCtx.getBean(FhirContext.class));
|
||||||
beans = myAppCtx.getBean("myResourceProvidersR4", ResourceProviderFactory.class);
|
beans = myAppCtx.getBean("myResourceProvidersR4", ResourceProviderFactory.class);
|
||||||
systemDao = myAppCtx.getBean("mySystemDaoR4", IFhirSystemDao.class);
|
systemDao = myAppCtx.getBean("mySystemDaoR4", IFhirSystemDao.class);
|
||||||
etagSupport = ETagSupportEnum.ENABLED;
|
etagSupport = ETagSupportEnum.ENABLED;
|
||||||
|
@ -155,6 +160,7 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
|
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
|
||||||
providers.add(myAppCtx.getBean(GraphQLProvider.class));
|
providers.add(myAppCtx.getBean(GraphQLProvider.class));
|
||||||
providers.add(myAppCtx.getBean(IpsOperationProvider.class));
|
providers.add(myAppCtx.getBean(IpsOperationProvider.class));
|
||||||
|
registerInterceptor(myAppCtx.getBean(BalpAuditCaptureInterceptor.class));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "R4B": {
|
case "R4B": {
|
||||||
|
@ -164,7 +170,7 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
myAppCtx.register(TestR4BConfig.class);
|
myAppCtx.register(TestR4BConfig.class);
|
||||||
baseUrlProperty = FHIR_BASEURL_R4B;
|
baseUrlProperty = FHIR_BASEURL_R4B;
|
||||||
myAppCtx.refresh();
|
myAppCtx.refresh();
|
||||||
setFhirContext(FhirContext.forR4BCached());
|
setFhirContext(myAppCtx.getBean(FhirContext.class));
|
||||||
beans = myAppCtx.getBean("myResourceProvidersR4B", ResourceProviderFactory.class);
|
beans = myAppCtx.getBean("myResourceProvidersR4B", ResourceProviderFactory.class);
|
||||||
systemDao = myAppCtx.getBean("mySystemDaoR4B", IFhirSystemDao.class);
|
systemDao = myAppCtx.getBean("mySystemDaoR4B", IFhirSystemDao.class);
|
||||||
etagSupport = ETagSupportEnum.ENABLED;
|
etagSupport = ETagSupportEnum.ENABLED;
|
||||||
|
@ -173,6 +179,7 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
setServerConformanceProvider(confProvider);
|
setServerConformanceProvider(confProvider);
|
||||||
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
|
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
|
||||||
providers.add(myAppCtx.getBean(GraphQLProvider.class));
|
providers.add(myAppCtx.getBean(GraphQLProvider.class));
|
||||||
|
registerInterceptor(myAppCtx.getBean(BalpAuditCaptureInterceptor.class));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "R5": {
|
case "R5": {
|
||||||
|
@ -182,7 +189,7 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
myAppCtx.register(TestR5Config.class, WebsocketDispatcherConfig.class);
|
myAppCtx.register(TestR5Config.class, WebsocketDispatcherConfig.class);
|
||||||
baseUrlProperty = FHIR_BASEURL_R5;
|
baseUrlProperty = FHIR_BASEURL_R5;
|
||||||
myAppCtx.refresh();
|
myAppCtx.refresh();
|
||||||
setFhirContext(FhirContext.forR5());
|
setFhirContext(myAppCtx.getBean(FhirContext.class));
|
||||||
beans = myAppCtx.getBean("myResourceProvidersR5", ResourceProviderFactory.class);
|
beans = myAppCtx.getBean("myResourceProvidersR5", ResourceProviderFactory.class);
|
||||||
systemDao = myAppCtx.getBean("mySystemDaoR5", IFhirSystemDao.class);
|
systemDao = myAppCtx.getBean("mySystemDaoR5", IFhirSystemDao.class);
|
||||||
etagSupport = ETagSupportEnum.ENABLED;
|
etagSupport = ETagSupportEnum.ENABLED;
|
||||||
|
@ -191,6 +198,23 @@ public class TestRestfulServer extends RestfulServer {
|
||||||
setServerConformanceProvider(confProvider);
|
setServerConformanceProvider(confProvider);
|
||||||
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
|
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
|
||||||
providers.add(myAppCtx.getBean(GraphQLProvider.class));
|
providers.add(myAppCtx.getBean(GraphQLProvider.class));
|
||||||
|
registerInterceptor(myAppCtx.getBean(BalpAuditCaptureInterceptor.class));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "AUDIT": {
|
||||||
|
myAppCtx = new AnnotationConfigWebApplicationContext();
|
||||||
|
myAppCtx.setServletConfig(getServletConfig());
|
||||||
|
myAppCtx.setParent(parentAppCtx);
|
||||||
|
myAppCtx.register(TestAuditConfig.class);
|
||||||
|
baseUrlProperty = FHIR_BASEURL_AUDIT;
|
||||||
|
myAppCtx.refresh();
|
||||||
|
setFhirContext(myAppCtx.getBean(FhirContext.class));
|
||||||
|
beans = myAppCtx.getBean("myResourceProvidersR4", ResourceProviderFactory.class);
|
||||||
|
systemDao = myAppCtx.getBean("mySystemDaoR4", IFhirSystemDao.class);
|
||||||
|
etagSupport = ETagSupportEnum.ENABLED;
|
||||||
|
IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class);
|
||||||
|
JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(this, systemDao, myAppCtx.getBean(JpaStorageSettings.class), myAppCtx.getBean(ISearchParamRegistry.class), validationSupport);
|
||||||
|
setServerConformanceProvider(confProvider);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
package ca.uhn.fhirtest.config;
|
package ca.uhn.fhirtest.config;
|
||||||
|
|
||||||
import ca.uhn.fhir.batch2.jobs.config.Batch2JobsConfig;
|
import ca.uhn.fhir.batch2.jobs.config.Batch2JobsConfig;
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
||||||
import ca.uhn.fhir.jpa.api.config.ThreadPoolFactoryConfig;
|
import ca.uhn.fhir.jpa.api.config.ThreadPoolFactoryConfig;
|
||||||
import ca.uhn.fhir.jpa.batch2.JpaBatch2Config;
|
import ca.uhn.fhir.jpa.batch2.JpaBatch2Config;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.AsyncMemoryQueueBackedFhirClientBalpSink;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.BalpAuditCaptureInterceptor;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.IBalpAuditContextServices;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.IBalpAuditEventSink;
|
||||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||||
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
|
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
|
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
|
||||||
|
@ -97,10 +102,6 @@ public class CommonConfig {
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isLocalTestMode() {
|
|
||||||
return "true".equalsIgnoreCase(System.getProperty("testmode.local"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ScheduledSubscriptionDeleter scheduledSubscriptionDeleter() {
|
public ScheduledSubscriptionDeleter scheduledSubscriptionDeleter() {
|
||||||
return new ScheduledSubscriptionDeleter();
|
return new ScheduledSubscriptionDeleter();
|
||||||
|
@ -111,4 +112,24 @@ public class CommonConfig {
|
||||||
return new CommonJpaStorageSettingsConfigurer(theStorageSettings);
|
return new CommonJpaStorageSettingsConfigurer(theStorageSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public IBalpAuditEventSink balpAuditEventSink(FhirContext theFhirContext) {
|
||||||
|
return new AsyncMemoryQueueBackedFhirClientBalpSink(theFhirContext, "http://localhost:8000/baseAudit");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public BalpAuditCaptureInterceptor balpAuditCaptureInterceptor(IBalpAuditEventSink theAuditSink, IBalpAuditContextServices theAuditContextServices) {
|
||||||
|
return new BalpAuditCaptureInterceptor(theAuditSink, theAuditContextServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public IBalpAuditContextServices balpContextServices() {
|
||||||
|
return new FhirTestBalpAuditContextServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLocalTestMode() {
|
||||||
|
return "true".equalsIgnoreCase(System.getProperty("testmode.local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package ca.uhn.fhirtest.config;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.IBalpAuditContextServices;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
|
import joptsimple.internal.Strings;
|
||||||
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||||
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is an implementation of the {@link IBalpAuditContextServices}
|
||||||
|
* interface for the public HAPI FHIR
|
||||||
|
*/
|
||||||
|
public class FhirTestBalpAuditContextServices implements IBalpAuditContextServices {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a Reference to the client that was used to
|
||||||
|
* perform the action in question.
|
||||||
|
* <p>
|
||||||
|
* In this case we are simply returning the HTTP User Agent (ie the name of the
|
||||||
|
* browser or HTTP client) because this server is anonymous. In a real implementation
|
||||||
|
* it would make more sense to return the identity of the SMART on FHIR
|
||||||
|
* client or something similar.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Reference getAgentClientWho(RequestDetails theRequestDetails) {
|
||||||
|
String userAgent = theRequestDetails.getHeader("User-Agent");
|
||||||
|
if (isBlank(userAgent)) {
|
||||||
|
userAgent = "Unknown User Agent";
|
||||||
|
}
|
||||||
|
Reference retVal = new Reference();
|
||||||
|
retVal.setDisplay(userAgent);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a Reference to the user that was used to
|
||||||
|
* perform the action in question.
|
||||||
|
* <p>
|
||||||
|
* In this case because this is an anoymous server we simply use
|
||||||
|
* a masked version of the user's IP as their identity. In a real
|
||||||
|
* server this should have details about the user account.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Reference getAgentUserWho(RequestDetails theRequestDetails) {
|
||||||
|
Reference retVal = new Reference();
|
||||||
|
retVal.setDisplay(getNetworkAddress(theRequestDetails));
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the requesting network address to include in the AuditEvent
|
||||||
|
* <p>
|
||||||
|
* Because this is a public server and these audit events will be visible
|
||||||
|
* to the outside world, we mask the latter half of the requesting IP
|
||||||
|
* address in order to not leak the identity of our users.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getNetworkAddress(RequestDetails theRequestDetails) {
|
||||||
|
ServletRequestDetails srd = (ServletRequestDetails) theRequestDetails;
|
||||||
|
|
||||||
|
String remoteAddr = defaultString(srd.getServletRequest().getRemoteAddr(), "UNKNOWN");
|
||||||
|
|
||||||
|
String[] parts = remoteAddr.split("\\.");
|
||||||
|
// Obscure part of the IP address
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
parts[2] = "X";
|
||||||
|
parts[3] = "X";
|
||||||
|
}
|
||||||
|
return Strings.join(parts, ".");
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,21 @@ public class FhirTesterConfig {
|
||||||
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||||
.withSearchResultRowOperation("$summary", id -> "Patient".equals(id.getResourceType()))
|
.withSearchResultRowOperation("$summary", id -> "Patient".equals(id.getResourceType()))
|
||||||
|
|
||||||
|
.addServer()
|
||||||
|
.withId("home_r5")
|
||||||
|
.withFhirVersion(FhirVersionEnum.R5)
|
||||||
|
.withBaseUrl("http://hapi.fhir.org/baseR5")
|
||||||
|
.withName("HAPI Test Server (R5 FHIR)")
|
||||||
|
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||||
|
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
||||||
|
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||||
|
|
||||||
|
.addServer()
|
||||||
|
.withId("home_audit")
|
||||||
|
.withFhirVersion(FhirVersionEnum.R4)
|
||||||
|
.withBaseUrl("http://hapi.fhir.org/baseAudit")
|
||||||
|
.withName("HAPI Test Server (R4 Audit)")
|
||||||
|
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("home_r4b")
|
.withId("home_r4b")
|
||||||
.withFhirVersion(FhirVersionEnum.R4B)
|
.withFhirVersion(FhirVersionEnum.R4B)
|
||||||
|
@ -77,15 +92,6 @@ public class FhirTesterConfig {
|
||||||
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||||
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||||
|
|
||||||
.addServer()
|
|
||||||
.withId("home_r5")
|
|
||||||
.withFhirVersion(FhirVersionEnum.R5)
|
|
||||||
.withBaseUrl("http://hapi.fhir.org/baseR5")
|
|
||||||
.withName("HAPI Test Server (R5 FHIR)")
|
|
||||||
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
|
||||||
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
|
||||||
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
|
||||||
|
|
||||||
// Non-HAPI servers follow
|
// Non-HAPI servers follow
|
||||||
|
|
||||||
.addServer()
|
.addServer()
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
package ca.uhn.fhirtest.config;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
||||||
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
|
import ca.uhn.fhir.jpa.config.HapiJpaConfig;
|
||||||
|
import ca.uhn.fhir.jpa.config.r4.JpaR4Config;
|
||||||
|
import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil;
|
||||||
|
import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy;
|
||||||
|
import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc;
|
||||||
|
import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl;
|
||||||
|
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider;
|
||||||
|
import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy;
|
||||||
|
import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect;
|
||||||
|
import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect;
|
||||||
|
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
|
||||||
|
import ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers;
|
||||||
|
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
|
||||||
|
import ca.uhn.fhir.jpa.validation.ValidationSettings;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
|
||||||
|
import ca.uhn.fhir.validation.IInstanceValidatorModule;
|
||||||
|
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||||
|
import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor;
|
||||||
|
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
||||||
|
import org.apache.commons.dbcp2.BasicDataSource;
|
||||||
|
import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings;
|
||||||
|
import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings;
|
||||||
|
import org.hibernate.search.engine.cfg.BackendSettings;
|
||||||
|
import org.hl7.fhir.dstu2.model.Subscription;
|
||||||
|
import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
|
||||||
|
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||||
|
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||||
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.persistence.EntityManagerFactory;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Import({CommonConfig.class, JpaR4Config.class, HapiJpaConfig.class})
|
||||||
|
@EnableTransactionManagement()
|
||||||
|
public class TestAuditConfig {
|
||||||
|
public static final Integer COUNT_SEARCH_RESULTS_UP_TO = 50000;
|
||||||
|
|
||||||
|
private String myDbUsername = System.getProperty(TestR5Config.FHIR_DB_USERNAME);
|
||||||
|
private String myDbPassword = System.getProperty(TestR5Config.FHIR_DB_PASSWORD);
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JpaStorageSettings storageSettings() {
|
||||||
|
JpaStorageSettings retVal = new JpaStorageSettings();
|
||||||
|
retVal.setAllowContainsSearches(true);
|
||||||
|
retVal.setAllowMultipleDelete(true);
|
||||||
|
retVal.setAllowInlineMatchUrlReferences(false);
|
||||||
|
retVal.setAllowExternalReferences(true);
|
||||||
|
retVal.getTreatBaseUrlsAsLocal().add("http://hapi.fhir.org/baseAudit");
|
||||||
|
retVal.getTreatBaseUrlsAsLocal().add("https://hapi.fhir.org/baseAudit");
|
||||||
|
retVal.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
|
||||||
|
retVal.setCountSearchResultsUpTo(TestAuditConfig.COUNT_SEARCH_RESULTS_UP_TO);
|
||||||
|
retVal.setFetchSizeDefaultMaximum(10000);
|
||||||
|
retVal.setExpungeEnabled(true);
|
||||||
|
retVal.setAllowExternalReferences(true);
|
||||||
|
retVal.setFilterParameterEnabled(true);
|
||||||
|
retVal.setDefaultSearchParamsCanBeOverridden(false);
|
||||||
|
retVal.setIndexOnContainedResources(false);
|
||||||
|
retVal.setIndexIdentifierOfType(false);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ValidationSettings validationSettings() {
|
||||||
|
ValidationSettings retVal = new ValidationSettings();
|
||||||
|
retVal.setLocalReferenceValidationDefaultPolicy(ReferenceValidationPolicy.CHECK_VALID);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Bean(name = "myPersistenceDataSourceR4")
|
||||||
|
public DataSource dataSource() {
|
||||||
|
BasicDataSource retVal = new BasicDataSource();
|
||||||
|
if (CommonConfig.isLocalTestMode()) {
|
||||||
|
retVal.setUrl("jdbc:h2:mem:fhirtest_audit");
|
||||||
|
} else {
|
||||||
|
retVal.setDriver(new org.postgresql.Driver());
|
||||||
|
retVal.setUrl("jdbc:postgresql://localhost/fhirtest_audit");
|
||||||
|
}
|
||||||
|
retVal.setUsername(myDbUsername);
|
||||||
|
retVal.setPassword(myDbPassword);
|
||||||
|
TestR5Config.applyCommonDatasourceParams(retVal);
|
||||||
|
|
||||||
|
DataSource dataSource = ProxyDataSourceBuilder
|
||||||
|
.create(retVal)
|
||||||
|
// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
|
||||||
|
.logSlowQueryBySlf4j(10000, TimeUnit.MILLISECONDS)
|
||||||
|
.afterQuery(new CurrentThreadCaptureQueriesListener())
|
||||||
|
.countQuery()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DatabaseBackedPagingProvider databaseBackedPagingProvider() {
|
||||||
|
DatabaseBackedPagingProvider retVal = new DatabaseBackedPagingProvider();
|
||||||
|
retVal.setDefaultPageSize(20);
|
||||||
|
retVal.setMaximumPageSize(500);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory, FhirContext theFhirContext) {
|
||||||
|
LocalContainerEntityManagerFactoryBean retVal = HapiEntityManagerFactoryUtil.newEntityManagerFactory(theConfigurableListableBeanFactory, theFhirContext);
|
||||||
|
retVal.setPersistenceUnitName("PU_HapiFhirJpaAudit");
|
||||||
|
retVal.setDataSource(dataSource());
|
||||||
|
retVal.setJpaProperties(jpaProperties());
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Properties jpaProperties() {
|
||||||
|
Properties extraProperties = new Properties();
|
||||||
|
if (CommonConfig.isLocalTestMode()) {
|
||||||
|
extraProperties.put("hibernate.dialect", HapiFhirH2Dialect.class.getName());
|
||||||
|
} else {
|
||||||
|
extraProperties.put("hibernate.dialect", HapiFhirPostgres94Dialect.class.getName());
|
||||||
|
}
|
||||||
|
extraProperties.put("hibernate.format_sql", "false");
|
||||||
|
extraProperties.put("hibernate.show_sql", "false");
|
||||||
|
extraProperties.put("hibernate.hbm2ddl.auto", "update");
|
||||||
|
extraProperties.put("hibernate.jdbc.batch_size", "20");
|
||||||
|
extraProperties.put("hibernate.cache.use_query_cache", "false");
|
||||||
|
extraProperties.put("hibernate.cache.use_second_level_cache", "false");
|
||||||
|
extraProperties.put("hibernate.cache.use_structured_entries", "false");
|
||||||
|
extraProperties.put("hibernate.cache.use_minimal_puts", "false");
|
||||||
|
extraProperties.put("hibernate.search.enabled", "false");
|
||||||
|
|
||||||
|
return extraProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PublicSecurityInterceptor securityInterceptor() {
|
||||||
|
return new PublicSecurityInterceptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
|
||||||
|
JpaTransactionManager retVal = new JpaTransactionManager();
|
||||||
|
retVal.setEntityManagerFactory(entityManagerFactory);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This lets the "@Value" fields reference properties from the properties file
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
|
||||||
|
return new PropertySourcesPlaceholderConfigurer();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DaoRegistryConfigurer daoRegistryConfigurer() {
|
||||||
|
return new DaoRegistryConfigurer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DaoRegistryConfigurer {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DaoRegistry myDaoRegistry;
|
||||||
|
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void start() {
|
||||||
|
myDaoRegistry.setSupportedResourceTypes("AuditEvent", "SearchParameter", "Subscription");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -46,6 +46,16 @@
|
||||||
<load-on-startup>1</load-on-startup>
|
<load-on-startup>1</load-on-startup>
|
||||||
</servlet>
|
</servlet>
|
||||||
|
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>fhirServletAudit</servlet-name>
|
||||||
|
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
|
||||||
|
<init-param>
|
||||||
|
<param-name>FhirVersion</param-name>
|
||||||
|
<param-value>Audit</param-value>
|
||||||
|
</init-param>
|
||||||
|
<load-on-startup>1</load-on-startup>
|
||||||
|
</servlet>
|
||||||
|
|
||||||
<servlet>
|
<servlet>
|
||||||
<servlet-name>fhirServletR4B</servlet-name>
|
<servlet-name>fhirServletR4B</servlet-name>
|
||||||
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
|
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
|
||||||
|
@ -98,6 +108,10 @@
|
||||||
<servlet-name>fhirServletR4</servlet-name>
|
<servlet-name>fhirServletR4</servlet-name>
|
||||||
<url-pattern>/baseR4/*</url-pattern>
|
<url-pattern>/baseR4/*</url-pattern>
|
||||||
</servlet-mapping>
|
</servlet-mapping>
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>fhirServletAudit</servlet-name>
|
||||||
|
<url-pattern>/baseAudit/*</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
<servlet-mapping>
|
<servlet-mapping>
|
||||||
<servlet-name>fhirServletDstu2</servlet-name>
|
<servlet-name>fhirServletDstu2</servlet-name>
|
||||||
<url-pattern>/baseDstu2/*</url-pattern>
|
<url-pattern>/baseDstu2/*</url-pattern>
|
||||||
|
|
|
@ -45,6 +45,7 @@ public class UhnFhirTestApp {
|
||||||
System.setProperty("fhir.baseurl.r4", base.replace("Dstu2", "R4"));
|
System.setProperty("fhir.baseurl.r4", base.replace("Dstu2", "R4"));
|
||||||
System.setProperty("fhir.baseurl.r4b", base.replace("Dstu2", "R4B"));
|
System.setProperty("fhir.baseurl.r4b", base.replace("Dstu2", "R4B"));
|
||||||
System.setProperty("fhir.baseurl.r5", base.replace("Dstu2", "R5"));
|
System.setProperty("fhir.baseurl.r5", base.replace("Dstu2", "R5"));
|
||||||
|
System.setProperty("fhir.baseurl.audit", base.replace("Dstu2", "Audit"));
|
||||||
System.setProperty("fhir.baseurl.tdl2", base.replace("baseDstu2", "testDataLibraryDstu2"));
|
System.setProperty("fhir.baseurl.tdl2", base.replace("baseDstu2", "testDataLibraryDstu2"));
|
||||||
System.setProperty("fhir.baseurl.tdl3", base.replace("baseDstu2", "testDataLibraryStu3"));
|
System.setProperty("fhir.baseurl.tdl3", base.replace("baseDstu2", "testDataLibraryStu3"));
|
||||||
System.setProperty("fhir.tdlpass", "aa,bb");
|
System.setProperty("fhir.tdlpass", "aa,bb");
|
||||||
|
|
|
@ -19,8 +19,11 @@
|
||||||
*/
|
*/
|
||||||
package ca.uhn.fhir.rest.api.server;
|
package ca.uhn.fhir.rest.api.server;
|
||||||
|
|
||||||
|
import org.apache.commons.collections4.IteratorUtils;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface is a parameter type for the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES}
|
* This interface is a parameter type for the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES}
|
||||||
* hook.
|
* hook.
|
||||||
|
@ -55,4 +58,16 @@ public interface IPreResourceShowDetails extends Iterable<IBaseResource> {
|
||||||
*/
|
*/
|
||||||
void markResourceAtIndexAsSubset(int theIndex);
|
void markResourceAtIndexAsSubset(int theIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link List} containing all resources that will be shown.
|
||||||
|
* The returned list will have the same relative ordering as if the resources
|
||||||
|
* were retrieved using {@link #getResource(int)}, but any {@literal null} entries
|
||||||
|
* will be filtered out.
|
||||||
|
* <p>
|
||||||
|
* The returned List may not be modified. Use this method only if you are not
|
||||||
|
* looking to make changes.
|
||||||
|
*
|
||||||
|
* @since 6.6.0
|
||||||
|
*/
|
||||||
|
List<IBaseResource> getAllResources();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,10 @@ import com.google.common.collect.Lists;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -76,6 +78,16 @@ public class SimplePreResourceShowDetails implements IPreResourceShowDetails {
|
||||||
myResourceMarkedAsSubset[theIndex] = true;
|
myResourceMarkedAsSubset[theIndex] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IBaseResource> getAllResources() {
|
||||||
|
ArrayList<IBaseResource> retVal = new ArrayList<>(myResources.length);
|
||||||
|
Arrays
|
||||||
|
.stream(myResources)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.forEach(retVal::add);
|
||||||
|
return Collections.unmodifiableList(retVal);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterator<IBaseResource> iterator() {
|
public Iterator<IBaseResource> iterator() {
|
||||||
return Arrays.asList(myResources).iterator();
|
return Arrays.asList(myResources).iterator();
|
||||||
|
|
|
@ -674,6 +674,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
@Nonnull
|
@Nonnull
|
||||||
protected ToStringBuilder toStringBuilder() {
|
protected ToStringBuilder toStringBuilder() {
|
||||||
ToStringBuilder builder = super.toStringBuilder();
|
ToStringBuilder builder = super.toStringBuilder();
|
||||||
|
|
|
@ -28,6 +28,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
||||||
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
|
import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
|
||||||
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
|
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
|
||||||
import ca.uhn.fhir.rest.annotation.Create;
|
import ca.uhn.fhir.rest.annotation.Create;
|
||||||
|
@ -35,27 +36,27 @@ import ca.uhn.fhir.rest.annotation.Delete;
|
||||||
import ca.uhn.fhir.rest.annotation.History;
|
import ca.uhn.fhir.rest.annotation.History;
|
||||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||||
import ca.uhn.fhir.rest.annotation.Read;
|
import ca.uhn.fhir.rest.annotation.Read;
|
||||||
import ca.uhn.fhir.rest.annotation.RequiredParam;
|
|
||||||
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
||||||
import ca.uhn.fhir.rest.annotation.Search;
|
import ca.uhn.fhir.rest.annotation.Search;
|
||||||
import ca.uhn.fhir.rest.annotation.Update;
|
import ca.uhn.fhir.rest.annotation.Update;
|
||||||
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
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.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
|
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
|
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
|
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
|
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
||||||
import ca.uhn.fhir.rest.param.TokenAndListParam;
|
|
||||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
|
||||||
import ca.uhn.fhir.rest.param.TokenParam;
|
|
||||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||||
|
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
import ca.uhn.fhir.util.ValidateUtil;
|
import ca.uhn.fhir.util.ValidateUtil;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.hl7.fhir.instance.model.api.IBase;
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
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.instance.model.api.IIdType;
|
||||||
|
@ -64,16 +65,12 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.lang.Math.min;
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -97,15 +94,15 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
private final Class<T> myResourceType;
|
private final Class<T> myResourceType;
|
||||||
private final FhirContext myFhirContext;
|
private final FhirContext myFhirContext;
|
||||||
private final String myResourceName;
|
private final String myResourceName;
|
||||||
|
private final AtomicLong myDeleteCount = new AtomicLong(0);
|
||||||
|
private final AtomicLong myUpdateCount = new AtomicLong(0);
|
||||||
|
private final AtomicLong myCreateCount = new AtomicLong(0);
|
||||||
|
private final AtomicLong myReadCount = new AtomicLong(0);
|
||||||
protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new LinkedHashMap<>();
|
protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new LinkedHashMap<>();
|
||||||
protected Map<String, LinkedList<T>> myIdToHistory = new LinkedHashMap<>();
|
protected Map<String, LinkedList<T>> myIdToHistory = new LinkedHashMap<>();
|
||||||
protected LinkedList<T> myTypeHistory = new LinkedList<>();
|
protected LinkedList<T> myTypeHistory = new LinkedList<>();
|
||||||
protected AtomicLong mySearchCount = new AtomicLong(0);
|
protected AtomicLong mySearchCount = new AtomicLong(0);
|
||||||
private long myNextId;
|
private long myNextId;
|
||||||
private final AtomicLong myDeleteCount = new AtomicLong(0);
|
|
||||||
private final AtomicLong myUpdateCount = new AtomicLong(0);
|
|
||||||
private final AtomicLong myCreateCount = new AtomicLong(0);
|
|
||||||
private final AtomicLong myReadCount = new AtomicLong(0);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -282,11 +279,47 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Search
|
@Search(allowUnknownParams = true)
|
||||||
public synchronized List<IBaseResource> searchAll(RequestDetails theRequestDetails) {
|
public synchronized IBundleProvider searchAll(RequestDetails theRequestDetails) {
|
||||||
mySearchCount.incrementAndGet();
|
mySearchCount.incrementAndGet();
|
||||||
List<T> retVal = getAllResources();
|
List<T> allResources = getAllResources();
|
||||||
return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails);
|
|
||||||
|
if (theRequestDetails.getParameters().containsKey(Constants.PARAM_ID)) {
|
||||||
|
for (String nextParam : theRequestDetails.getParameters().get(Constants.PARAM_ID)) {
|
||||||
|
List<IdDt> wantIds = Arrays.stream(nextParam.split(","))
|
||||||
|
.map(StringUtils::trim)
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.map(IdDt::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (Iterator<T> iter = allResources.iterator(); iter.hasNext(); ) {
|
||||||
|
T next = iter.next();
|
||||||
|
boolean found = wantIds
|
||||||
|
.stream()
|
||||||
|
.anyMatch(t -> resourceIdMatches(next, t));
|
||||||
|
if (!found) {
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SimpleBundleProvider(allResources) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
|
||||||
|
|
||||||
|
// Make sure that "from" isn't less than 0, "to" isn't more than the number available,
|
||||||
|
// and "from" <= "to"
|
||||||
|
int from = max(0, theFromIndex);
|
||||||
|
int to = min(theToIndex, allResources.size());
|
||||||
|
to = max(from, to);
|
||||||
|
|
||||||
|
List<IBaseResource> retVal = (List<IBaseResource>) allResources.subList(from, to);
|
||||||
|
retVal = fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
@ -310,44 +343,6 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Search
|
|
||||||
public synchronized List<IBaseResource> searchById(
|
|
||||||
@RequiredParam(name = "_id") TokenAndListParam theIds, RequestDetails theRequestDetails) {
|
|
||||||
|
|
||||||
List<T> retVal = new ArrayList<>();
|
|
||||||
|
|
||||||
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
|
|
||||||
if (next.isEmpty() == false) {
|
|
||||||
T nextResource = next.lastEntry().getValue();
|
|
||||||
|
|
||||||
boolean matches = true;
|
|
||||||
if (theIds != null && theIds.getValuesAsQueryTokens().size() > 0) {
|
|
||||||
for (TokenOrListParam nextIdAnd : theIds.getValuesAsQueryTokens()) {
|
|
||||||
matches = false;
|
|
||||||
for (TokenParam nextOr : nextIdAnd.getValuesAsQueryTokens()) {
|
|
||||||
if (nextOr.getValue().equals(nextResource.getIdElement().getIdPart())) {
|
|
||||||
matches = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!matches) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
retVal.add(nextResource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mySearchCount.incrementAndGet();
|
|
||||||
|
|
||||||
return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "DataFlowIssue"})
|
@SuppressWarnings({"unchecked", "DataFlowIssue"})
|
||||||
private IIdType store(@Nonnull T theResource, String theIdPart, Long theVersionIdPart, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean theDeleted) {
|
private IIdType store(@Nonnull T theResource, String theIdPart, Long theVersionIdPart, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean theDeleted) {
|
||||||
IIdType id = myFhirContext.getVersion().newIdType();
|
IIdType id = myFhirContext.getVersion().newIdType();
|
||||||
|
@ -386,10 +381,6 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
|
|
||||||
ourLog.info("Storing resource with ID: {}", id.getValue());
|
ourLog.info("Storing resource with ID: {}", id.getValue());
|
||||||
|
|
||||||
// Store to ID->version->resource map
|
|
||||||
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
|
|
||||||
versionToResource.put(theVersionIdPart, theResource);
|
|
||||||
|
|
||||||
if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null) {
|
if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null) {
|
||||||
IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster();
|
IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster();
|
||||||
|
|
||||||
|
@ -457,6 +448,10 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store to ID->version->resource map
|
||||||
|
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
|
||||||
|
versionToResource.put(theVersionIdPart, theResource);
|
||||||
|
|
||||||
// Store to type history map
|
// Store to type history map
|
||||||
myTypeHistory.addFirst(theResource);
|
myTypeHistory.addFirst(theResource);
|
||||||
|
|
||||||
|
@ -542,6 +537,15 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
return Collections.unmodifiableList(retVal);
|
return Collections.unmodifiableList(retVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean resourceIdMatches(T theResource, IdDt theId) {
|
||||||
|
if (theId.getResourceType() == null || theId.getResourceType().equals(myFhirContext.getResourceType(theResource))) {
|
||||||
|
if (theResource.getIdElement().getIdPart().equals(theId.getIdPart())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static <T extends IBaseResource> T fireInterceptorsAndFilterAsNeeded(T theResource, RequestDetails theRequestDetails) {
|
private static <T extends IBaseResource> T fireInterceptorsAndFilterAsNeeded(T theResource, RequestDetails theRequestDetails) {
|
||||||
List<IBaseResource> output = fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails);
|
List<IBaseResource> output = fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails);
|
||||||
if (output.size() == 1) {
|
if (output.size() == 1) {
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
package ca.uhn.fhir.rest.api.server;
|
package ca.uhn.fhir.rest.api.server;
|
||||||
|
|
||||||
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.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
@ -51,4 +50,16 @@ public class SimplePreResourceShowDetailsTest {
|
||||||
details.setResource(0, myResource2);
|
details.setResource(0, myResource2);
|
||||||
assertSame(myResource2, details.iterator().next());
|
assertSame(myResource2, details.iterator().next());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetResources() {
|
||||||
|
SimplePreResourceShowDetails details = new SimplePreResourceShowDetails(List.of(myResource1, myResource2));
|
||||||
|
assertThat(details.getAllResources(), contains(myResource1, myResource2));
|
||||||
|
|
||||||
|
details.setResource(0, null);
|
||||||
|
|
||||||
|
assertThat(details.getAllResources(), contains(myResource2));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation of the {@link IBalpAuditEventSink} transmits audit events to
|
||||||
|
* a FHIR endpoint for creation, using a standard fhir <i>create</i> event. The target
|
||||||
|
* server FHIR version does not need to match the FHIR version of the AuditEvent source,
|
||||||
|
* events will be converted automatically prior to sending.
|
||||||
|
* <p>
|
||||||
|
* This sink transmits events asynchronously using an in-memory queue. This means that
|
||||||
|
* in the event of a server shutdown data could be lost.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class AsyncMemoryQueueBackedFhirClientBalpSink extends FhirClientBalpSink implements IBalpAuditEventSink {
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(AsyncMemoryQueueBackedFhirClientBalpSink.class);
|
||||||
|
private final ArrayBlockingQueue<IBaseResource> myQueue;
|
||||||
|
private static final AtomicLong ourNextThreadId = new AtomicLong(0);
|
||||||
|
private boolean myRunning;
|
||||||
|
private TransmitterThread myThread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the FhirContext to use when initiating outgoing connections
|
||||||
|
*
|
||||||
|
* @param theFhirContext The FhirContext instance. This context must be
|
||||||
|
* for the FHIR Version supported by the target/sink
|
||||||
|
* server (as opposed to the FHIR Version supported
|
||||||
|
* by the audit source).
|
||||||
|
* @param theTargetBaseUrl The FHIR server base URL for the target/sink server to
|
||||||
|
* receive audit events.
|
||||||
|
*/
|
||||||
|
public AsyncMemoryQueueBackedFhirClientBalpSink(@Nonnull FhirContext theFhirContext, @Nonnull String theTargetBaseUrl) {
|
||||||
|
this(theFhirContext, theTargetBaseUrl, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the FhirContext to use when initiating outgoing connections
|
||||||
|
*
|
||||||
|
* @param theFhirContext The FhirContext instance. This context must be
|
||||||
|
* for the FHIR Version supported by the target/sink
|
||||||
|
* server (as opposed to the FHIR Version supported
|
||||||
|
* by the audit source).
|
||||||
|
* @param theTargetBaseUrl The FHIR server base URL for the target/sink server to
|
||||||
|
* receive audit events.
|
||||||
|
* @param theClientInterceptors An optional list of interceptors to register against
|
||||||
|
* the client. May be {@literal null}.
|
||||||
|
*/
|
||||||
|
public AsyncMemoryQueueBackedFhirClientBalpSink(@Nonnull FhirContext theFhirContext, @Nonnull String theTargetBaseUrl, @Nullable List<Object> theClientInterceptors) {
|
||||||
|
this(createClient(theFhirContext, theTargetBaseUrl, theClientInterceptors));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param theClient The FHIR client to use as a sink.
|
||||||
|
*/
|
||||||
|
public AsyncMemoryQueueBackedFhirClientBalpSink(IGenericClient theClient) {
|
||||||
|
super(theClient);
|
||||||
|
myQueue = new ArrayBlockingQueue<>(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void recordAuditEvent(IBaseResource theAuditEvent) {
|
||||||
|
myQueue.add(theAuditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void start() {
|
||||||
|
if (!myRunning) {
|
||||||
|
myRunning = true;
|
||||||
|
myThread = new TransmitterThread();
|
||||||
|
myThread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stop() {
|
||||||
|
if (myRunning) {
|
||||||
|
myRunning = false;
|
||||||
|
myThread.interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() {
|
||||||
|
return myThread != null && myThread.isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TransmitterThread extends Thread {
|
||||||
|
|
||||||
|
private boolean myThreadRunning;
|
||||||
|
|
||||||
|
public TransmitterThread() {
|
||||||
|
setName("BalpClientSink-" + ourNextThreadId.getAndIncrement());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ourLog.info("Starting BALP Client Sink Transmitter");
|
||||||
|
myThreadRunning = true;
|
||||||
|
while (myRunning) {
|
||||||
|
IBaseResource next = null;
|
||||||
|
try {
|
||||||
|
next = myQueue.poll(10, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Currently we transmit events one by one, but a nice optimization
|
||||||
|
// would be to batch them into FHIR transaction Bundles. If we do this, we
|
||||||
|
// would get better performance, but we'd also want to have some retry
|
||||||
|
// logic that submits events individually if a transaction fails.
|
||||||
|
|
||||||
|
if (next != null) {
|
||||||
|
try {
|
||||||
|
transmitEventToClient(next);
|
||||||
|
} catch (Exception e) {
|
||||||
|
ourLog.warn("Failed to transmit AuditEvent to sink: {}", e.toString());
|
||||||
|
myQueue.add(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
ourLog.info("Stopping BALP Client Sink Transmitter");
|
||||||
|
myThreadRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() {
|
||||||
|
return myThreadRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,393 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
|
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
|
import ca.uhn.fhir.util.FhirTerser;
|
||||||
|
import ca.uhn.fhir.util.UrlUtil;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.r4.model.AuditEvent;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IHE Basic Audit Logging Pattern (BALP) interceptor can be used to autopmatically generate
|
||||||
|
* AuditEvent resources that are conformant to the BALP profile in response to events in a
|
||||||
|
* FHIR server. See <a href="https://hapifhir.io/hapi-fhir/security/balp_interceptor.html">BALP Interceptor</a>
|
||||||
|
* in the HAPI FHIR documentation for more information.
|
||||||
|
*
|
||||||
|
* @since 6.6.0
|
||||||
|
*/
|
||||||
|
public class BalpAuditCaptureInterceptor {
|
||||||
|
|
||||||
|
private final IBalpAuditEventSink myAuditEventSink;
|
||||||
|
private final IBalpAuditContextServices myContextServices;
|
||||||
|
private Set<String> myAdditionalPatientCompartmentParamNames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param theAuditEventSink This service is the target for generated AuditEvent resources. The {@link BalpAuditCaptureInterceptor}
|
||||||
|
* does not actually store AuditEvents, it simply generates them when appropriate and passes them to
|
||||||
|
* the sink service. The sink service might store them locally, transmit them to a remote
|
||||||
|
* repository, or even simply log them to a syslog.
|
||||||
|
* @param theContextServices This service supplies details to the BALP about the context of a given request. For example,
|
||||||
|
* in order to generate a conformant AuditEvent resource, this interceptor needs to determine the
|
||||||
|
* identity of the user and the client from the {@link ca.uhn.fhir.rest.api.server.RequestDetails}
|
||||||
|
* object.
|
||||||
|
*/
|
||||||
|
public BalpAuditCaptureInterceptor(@Nonnull IBalpAuditEventSink theAuditEventSink, @Nonnull IBalpAuditContextServices theContextServices) {
|
||||||
|
Validate.notNull(theAuditEventSink);
|
||||||
|
Validate.notNull(theContextServices);
|
||||||
|
myAuditEventSink = theAuditEventSink;
|
||||||
|
myContextServices = theContextServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addEntityPatient(AuditEvent theAuditEvent, String thePatientId) {
|
||||||
|
AuditEvent.AuditEventEntityComponent entityPatient = theAuditEvent.addEntity();
|
||||||
|
entityPatient
|
||||||
|
.getType()
|
||||||
|
.setSystem(BalpConstants.CS_AUDIT_ENTITY_TYPE)
|
||||||
|
.setCode(BalpConstants.CS_AUDIT_ENTITY_TYPE_1_PERSON)
|
||||||
|
.setDisplay(BalpConstants.CS_AUDIT_ENTITY_TYPE_1_PERSON_DISPLAY);
|
||||||
|
entityPatient
|
||||||
|
.getRole()
|
||||||
|
.setSystem(BalpConstants.CS_OBJECT_ROLE)
|
||||||
|
.setCode(BalpConstants.CS_OBJECT_ROLE_1_PATIENT)
|
||||||
|
.setDisplay(BalpConstants.CS_OBJECT_ROLE_1_PATIENT_DISPLAY);
|
||||||
|
entityPatient
|
||||||
|
.getWhat()
|
||||||
|
.setReference(thePatientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addEntityData(AuditEvent theAuditEvent, String theDataResourceId) {
|
||||||
|
AuditEvent.AuditEventEntityComponent entityData = theAuditEvent.addEntity();
|
||||||
|
entityData
|
||||||
|
.getType()
|
||||||
|
.setSystem(BalpConstants.CS_AUDIT_ENTITY_TYPE)
|
||||||
|
.setCode(BalpConstants.CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT)
|
||||||
|
.setDisplay(BalpConstants.CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT_DISPLAY);
|
||||||
|
entityData
|
||||||
|
.getRole()
|
||||||
|
.setSystem(BalpConstants.CS_OBJECT_ROLE)
|
||||||
|
.setCode(BalpConstants.CS_OBJECT_ROLE_4_DOMAIN_RESOURCE)
|
||||||
|
.setDisplay(BalpConstants.CS_OBJECT_ROLE_4_DOMAIN_RESOURCE_DISPLAY);
|
||||||
|
entityData
|
||||||
|
.getWhat()
|
||||||
|
.setReference(theDataResourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdditionalPatientCompartmentParamNames(Set<String> theAdditionalPatientCompartmentParamNames) {
|
||||||
|
myAdditionalPatientCompartmentParamNames = theAdditionalPatientCompartmentParamNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interceptor hook method. Do not call directly.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
|
||||||
|
@Hook(Pointcut.STORAGE_PRESHOW_RESOURCES)
|
||||||
|
void hookStoragePreShowResources(IPreResourceShowDetails theDetails, ServletRequestDetails theRequestDetails) {
|
||||||
|
switch (theRequestDetails.getRestOperationType()) {
|
||||||
|
case SEARCH_TYPE:
|
||||||
|
case SEARCH_SYSTEM:
|
||||||
|
case GET_PAGE:
|
||||||
|
handleSearch(theDetails, theRequestDetails);
|
||||||
|
break;
|
||||||
|
case READ:
|
||||||
|
case VREAD:
|
||||||
|
handleReadOrVRead(theDetails, theRequestDetails);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// No actions for other operations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
|
||||||
|
public void hookStoragePrecommitResourceCreated(IBaseResource theResource, ServletRequestDetails theRequestDetails) {
|
||||||
|
handleCreateUpdateDelete(theResource, theRequestDetails, BalpProfileEnum.BASIC_CREATE, BalpProfileEnum.PATIENT_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
|
||||||
|
public void hookStoragePrecommitResourceDeleted(IBaseResource theResource, ServletRequestDetails theRequestDetails) {
|
||||||
|
handleCreateUpdateDelete(theResource, theRequestDetails, BalpProfileEnum.BASIC_DELETE, BalpProfileEnum.PATIENT_DELETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
|
||||||
|
public void hookStoragePrecommitResourceUpdated(IBaseResource theOldResource, IBaseResource theResource, ServletRequestDetails theRequestDetails) {
|
||||||
|
handleCreateUpdateDelete(theResource, theRequestDetails, BalpProfileEnum.BASIC_UPDATE, BalpProfileEnum.PATIENT_UPDATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCreateUpdateDelete(IBaseResource theResource, ServletRequestDetails theRequestDetails, BalpProfileEnum theBasicProfile, BalpProfileEnum thePatientProfile) {
|
||||||
|
Set<String> patientCompartmentOwners = determinePatientCompartmentOwnersForResources(List.of(theResource), theRequestDetails);
|
||||||
|
if (patientCompartmentOwners.isEmpty()) {
|
||||||
|
AuditEvent auditEvent = createAuditEventBasicCreateUpdateDelete(theRequestDetails, theResource, theBasicProfile);
|
||||||
|
myAuditEventSink.recordAuditEvent(auditEvent);
|
||||||
|
} else {
|
||||||
|
AuditEvent auditEvent = createAuditEventPatientCreateUpdateDelete(theRequestDetails, theResource, patientCompartmentOwners, thePatientProfile);
|
||||||
|
myAuditEventSink.recordAuditEvent(auditEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleReadOrVRead(IPreResourceShowDetails theDetails, ServletRequestDetails theRequestDetails) {
|
||||||
|
Validate.isTrue(theDetails.size() == 1, "Unexpected number of results for read: %d", theDetails.size());
|
||||||
|
IBaseResource resource = theDetails.getResource(0);
|
||||||
|
if (resource != null) {
|
||||||
|
String dataResourceId = myContextServices.massageResourceIdForStorage(theRequestDetails, resource, resource.getIdElement());
|
||||||
|
Set<String> patientIds = determinePatientCompartmentOwnersForResources(List.of(resource), theRequestDetails);
|
||||||
|
|
||||||
|
// If the resource is in the Patient compartment, create one audit
|
||||||
|
// event for each compartment owner
|
||||||
|
for (String patientId : patientIds) {
|
||||||
|
AuditEvent auditEvent = createAuditEventPatientRead(theRequestDetails, dataResourceId, patientId);
|
||||||
|
myAuditEventSink.recordAuditEvent(auditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, this is a basic read so create a basic read audit event
|
||||||
|
if (patientIds.isEmpty()) {
|
||||||
|
AuditEvent auditEvent = createAuditEventBasicRead(theRequestDetails, dataResourceId);
|
||||||
|
myAuditEventSink.recordAuditEvent(auditEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSearch(IPreResourceShowDetails theDetails, ServletRequestDetails theRequestDetails) {
|
||||||
|
|
||||||
|
List<IBaseResource> resources = theDetails.getAllResources();
|
||||||
|
Set<String> compartmentOwners = determinePatientCompartmentOwnersForResources(resources, theRequestDetails);
|
||||||
|
|
||||||
|
if (!compartmentOwners.isEmpty()) {
|
||||||
|
AuditEvent auditEvent = createAuditEventPatientQuery(theRequestDetails, compartmentOwners);
|
||||||
|
myAuditEventSink.recordAuditEvent(auditEvent);
|
||||||
|
} else {
|
||||||
|
AuditEvent auditEvent = createAuditEventBasicQuery(theRequestDetails);
|
||||||
|
myAuditEventSink.recordAuditEvent(auditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Set<String> determinePatientCompartmentOwnersForResources(List<IBaseResource> theResources, ServletRequestDetails theRequestDetails) {
|
||||||
|
Set<String> patientIds = new TreeSet<>();
|
||||||
|
FhirContext fhirContext = theRequestDetails.getFhirContext();
|
||||||
|
|
||||||
|
for (IBaseResource resource : theResources) {
|
||||||
|
RuntimeResourceDefinition resourceDef = fhirContext.getResourceDefinition(resource);
|
||||||
|
if (resourceDef.getName().equals("Patient")) {
|
||||||
|
patientIds.add(myContextServices.massageResourceIdForStorage(theRequestDetails, resource, resource.getIdElement()));
|
||||||
|
} else {
|
||||||
|
List<RuntimeSearchParam> compartmentSearchParameters = resourceDef.getSearchParamsForCompartmentName("Patient");
|
||||||
|
if (!compartmentSearchParameters.isEmpty()) {
|
||||||
|
FhirTerser terser = fhirContext.newTerser();
|
||||||
|
terser
|
||||||
|
.getCompartmentOwnersForResource("Patient", resource, myAdditionalPatientCompartmentParamNames)
|
||||||
|
.stream()
|
||||||
|
.map(t -> myContextServices.massageResourceIdForStorage(theRequestDetails, resource, t))
|
||||||
|
.forEach(patientIds::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patientIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventCommonCreate(ServletRequestDetails theRequestDetails, IBaseResource theResource, BalpProfileEnum profile) {
|
||||||
|
AuditEvent auditEvent = createAuditEventCommon(theRequestDetails, profile);
|
||||||
|
|
||||||
|
String resourceId = myContextServices.massageResourceIdForStorage(theRequestDetails, theResource, theResource.getIdElement());
|
||||||
|
addEntityData(auditEvent, resourceId);
|
||||||
|
return auditEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventBasicCreateUpdateDelete(ServletRequestDetails theRequestDetails, IBaseResource theResource, BalpProfileEnum theProfile) {
|
||||||
|
return createAuditEventCommonCreate(theRequestDetails, theResource, theProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventBasicQuery(ServletRequestDetails theRequestDetails) {
|
||||||
|
BalpProfileEnum profile = BalpProfileEnum.BASIC_QUERY;
|
||||||
|
AuditEvent auditEvent = createAuditEventCommonQuery(theRequestDetails, profile);
|
||||||
|
return auditEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventBasicRead(ServletRequestDetails theRequestDetails, String dataResourceId) {
|
||||||
|
return createAuditEventCommonRead(theRequestDetails, dataResourceId, BalpProfileEnum.BASIC_READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventPatientCreateUpdateDelete(ServletRequestDetails theRequestDetails, IBaseResource theResource, Set<String> thePatientCompartmentOwners, BalpProfileEnum theProfile) {
|
||||||
|
AuditEvent retVal = createAuditEventCommonCreate(theRequestDetails, theResource, theProfile);
|
||||||
|
for (String next : thePatientCompartmentOwners) {
|
||||||
|
addEntityPatient(retVal, next);
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventPatientQuery(ServletRequestDetails theRequestDetails, Set<String> compartmentOwners) {
|
||||||
|
BalpProfileEnum profile = BalpProfileEnum.PATIENT_QUERY;
|
||||||
|
AuditEvent auditEvent = createAuditEventCommonQuery(theRequestDetails, profile);
|
||||||
|
for (String next : compartmentOwners) {
|
||||||
|
addEntityPatient(auditEvent, next);
|
||||||
|
}
|
||||||
|
return auditEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventPatientRead(ServletRequestDetails theRequestDetails, String dataResourceId, String patientId) {
|
||||||
|
BalpProfileEnum profile = BalpProfileEnum.PATIENT_READ;
|
||||||
|
AuditEvent auditEvent = createAuditEventCommonRead(theRequestDetails, dataResourceId, profile);
|
||||||
|
addEntityPatient(auditEvent, patientId);
|
||||||
|
return auditEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventCommon(ServletRequestDetails theRequestDetails, BalpProfileEnum theProfile) {
|
||||||
|
RestOperationTypeEnum restOperationType = theRequestDetails.getRestOperationType();
|
||||||
|
if (restOperationType == RestOperationTypeEnum.GET_PAGE) {
|
||||||
|
restOperationType = RestOperationTypeEnum.SEARCH_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditEvent auditEvent = new AuditEvent();
|
||||||
|
auditEvent.getMeta().addProfile(theProfile.getProfileUrl());
|
||||||
|
auditEvent.getType()
|
||||||
|
.setSystem(BalpConstants.CS_AUDIT_EVENT_TYPE)
|
||||||
|
.setCode("rest")
|
||||||
|
.setDisplay("Restful Operation");
|
||||||
|
auditEvent.addSubtype()
|
||||||
|
.setSystem(BalpConstants.CS_RESTFUL_INTERACTION)
|
||||||
|
.setCode(restOperationType.getCode())
|
||||||
|
.setDisplay(restOperationType.getCode());
|
||||||
|
auditEvent.setAction(theProfile.getAction());
|
||||||
|
auditEvent.setOutcome(AuditEvent.AuditEventOutcome._0);
|
||||||
|
auditEvent.setRecorded(new Date());
|
||||||
|
|
||||||
|
auditEvent
|
||||||
|
.getSource()
|
||||||
|
.getObserver()
|
||||||
|
.setDisplay(theRequestDetails.getServerBaseForRequest());
|
||||||
|
|
||||||
|
AuditEvent.AuditEventAgentComponent clientAgent = auditEvent.addAgent();
|
||||||
|
clientAgent.setWho(myContextServices.getAgentClientWho(theRequestDetails));
|
||||||
|
clientAgent
|
||||||
|
.getType()
|
||||||
|
.addCoding(theProfile.getAgentClientTypeCoding());
|
||||||
|
clientAgent
|
||||||
|
.getWho()
|
||||||
|
.setDisplay(myContextServices.getNetworkAddress(theRequestDetails));
|
||||||
|
clientAgent
|
||||||
|
.getNetwork()
|
||||||
|
.setAddress(myContextServices.getNetworkAddress(theRequestDetails))
|
||||||
|
.setType(myContextServices.getNetworkAddressType(theRequestDetails));
|
||||||
|
clientAgent.setRequestor(false);
|
||||||
|
|
||||||
|
AuditEvent.AuditEventAgentComponent serverAgent = auditEvent.addAgent();
|
||||||
|
serverAgent
|
||||||
|
.getType()
|
||||||
|
.addCoding(theProfile.getAgentServerTypeCoding());
|
||||||
|
serverAgent
|
||||||
|
.getWho()
|
||||||
|
.setDisplay(theRequestDetails.getServerBaseForRequest());
|
||||||
|
serverAgent
|
||||||
|
.getNetwork()
|
||||||
|
.setAddress(theRequestDetails.getServerBaseForRequest());
|
||||||
|
serverAgent.setRequestor(false);
|
||||||
|
|
||||||
|
AuditEvent.AuditEventAgentComponent userAgent = auditEvent.addAgent();
|
||||||
|
userAgent
|
||||||
|
.getType()
|
||||||
|
.addCoding()
|
||||||
|
.setSystem("http://terminology.hl7.org/CodeSystem/v3-ParticipationType")
|
||||||
|
.setCode("IRCP")
|
||||||
|
.setDisplay("information recipient");
|
||||||
|
userAgent.setWho(myContextServices.getAgentUserWho(theRequestDetails));
|
||||||
|
userAgent
|
||||||
|
.setRequestor(true);
|
||||||
|
|
||||||
|
AuditEvent.AuditEventEntityComponent entityTransaction = auditEvent.addEntity();
|
||||||
|
entityTransaction
|
||||||
|
.getType()
|
||||||
|
.setSystem("https://profiles.ihe.net/ITI/BALP/CodeSystem/BasicAuditEntityType")
|
||||||
|
.setCode("XrequestId");
|
||||||
|
entityTransaction
|
||||||
|
.getWhat()
|
||||||
|
.getIdentifier()
|
||||||
|
.setValue(theRequestDetails.getRequestId());
|
||||||
|
return auditEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventCommonQuery(ServletRequestDetails theRequestDetails, BalpProfileEnum profile) {
|
||||||
|
AuditEvent auditEvent = createAuditEventCommon(theRequestDetails, profile);
|
||||||
|
|
||||||
|
AuditEvent.AuditEventEntityComponent queryEntity = auditEvent.addEntity();
|
||||||
|
queryEntity
|
||||||
|
.getType()
|
||||||
|
.setSystem(BalpConstants.CS_AUDIT_ENTITY_TYPE)
|
||||||
|
.setCode(BalpConstants.CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT)
|
||||||
|
.setDisplay(BalpConstants.CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT_DISPLAY);
|
||||||
|
queryEntity
|
||||||
|
.getRole()
|
||||||
|
.setSystem(BalpConstants.CS_OBJECT_ROLE)
|
||||||
|
.setCode(BalpConstants.CS_OBJECT_ROLE_24_QUERY)
|
||||||
|
.setDisplay(BalpConstants.CS_OBJECT_ROLE_24_QUERY_DISPLAY);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
StringBuilder description = new StringBuilder();
|
||||||
|
HttpServletRequest servletRequest = theRequestDetails.getServletRequest();
|
||||||
|
description.append(servletRequest.getMethod());
|
||||||
|
description.append(" ");
|
||||||
|
description.append(servletRequest.getRequestURI());
|
||||||
|
if (isNotBlank(servletRequest.getQueryString())) {
|
||||||
|
description.append("?");
|
||||||
|
description.append(servletRequest.getQueryString());
|
||||||
|
}
|
||||||
|
queryEntity.setDescription(description.toString());
|
||||||
|
|
||||||
|
// Query String
|
||||||
|
StringBuilder queryString = new StringBuilder();
|
||||||
|
queryString.append(theRequestDetails.getServerBaseForRequest());
|
||||||
|
queryString.append("/");
|
||||||
|
queryString.append(theRequestDetails.getRequestPath());
|
||||||
|
boolean first = true;
|
||||||
|
for (Map.Entry<String, String[]> nextEntrySet : theRequestDetails.getParameters().entrySet()) {
|
||||||
|
for (String nextValue : nextEntrySet.getValue()) {
|
||||||
|
if (first) {
|
||||||
|
queryString.append("?");
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
queryString.append("&");
|
||||||
|
}
|
||||||
|
queryString.append(UrlUtil.escapeUrlParam(nextEntrySet.getKey()));
|
||||||
|
queryString.append("=");
|
||||||
|
queryString.append(UrlUtil.escapeUrlParam(nextValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queryEntity
|
||||||
|
.getQueryElement()
|
||||||
|
.setValue(queryString.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
return auditEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private AuditEvent createAuditEventCommonRead(ServletRequestDetails theRequestDetails, String theDataResourceId, BalpProfileEnum theProfile) {
|
||||||
|
AuditEvent auditEvent = createAuditEventCommon(theRequestDetails, theProfile);
|
||||||
|
addEntityData(auditEvent, theDataResourceId);
|
||||||
|
return auditEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import org.hl7.fhir.r4.model.AuditEvent;
|
||||||
|
|
||||||
|
public class BalpConstants {
|
||||||
|
/**
|
||||||
|
* Constant for {@link AuditEvent.AuditEventAgentNetworkType} representing the code
|
||||||
|
* <code>1 - Machine name</code>. This constant is used only for convenience since the
|
||||||
|
* existing Enum uses numerical codes that are not great for readability.
|
||||||
|
*/
|
||||||
|
public static final AuditEvent.AuditEventAgentNetworkType AUDIT_EVENT_AGENT_NETWORK_TYPE_MACHINE_NAME = AuditEvent.AuditEventAgentNetworkType._1;
|
||||||
|
/**
|
||||||
|
* Constant for {@link AuditEvent.AuditEventAgentNetworkType} representing the code
|
||||||
|
* <code>2 - IP Address</code>. This constant is used only for convenience since the
|
||||||
|
* existing Enum uses numerical codes that are not great for readability.
|
||||||
|
*/
|
||||||
|
public static final AuditEvent.AuditEventAgentNetworkType AUDIT_EVENT_AGENT_NETWORK_TYPE_IP_ADDRESS = AuditEvent.AuditEventAgentNetworkType._2;
|
||||||
|
/**
|
||||||
|
* Constant for {@link AuditEvent.AuditEventAgentNetworkType} representing the code
|
||||||
|
* <code>3 - URI</code>. This constant is used only for convenience since the
|
||||||
|
* existing Enum uses numerical codes that are not great for readability.
|
||||||
|
*/
|
||||||
|
public static final AuditEvent.AuditEventAgentNetworkType AUDIT_EVENT_AGENT_NETWORK_TYPE_URI = AuditEvent.AuditEventAgentNetworkType._5;
|
||||||
|
|
||||||
|
public static final String CS_AUDIT_EVENT_TYPE = "http://terminology.hl7.org/CodeSystem/audit-event-type";
|
||||||
|
public static final String CS_AUDIT_ENTITY_TYPE = "http://terminology.hl7.org/CodeSystem/audit-entity-type";
|
||||||
|
public static final String CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT = "2";
|
||||||
|
public static final String CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT_DISPLAY = "System Object";
|
||||||
|
public static final String CS_AUDIT_ENTITY_TYPE_1_PERSON = "1";
|
||||||
|
public static final String CS_AUDIT_ENTITY_TYPE_1_PERSON_DISPLAY = "Person";
|
||||||
|
public static final String CS_OBJECT_ROLE = "http://terminology.hl7.org/CodeSystem/object-role";
|
||||||
|
public static final String CS_OBJECT_ROLE_1_PATIENT = "1";
|
||||||
|
public static final String CS_OBJECT_ROLE_1_PATIENT_DISPLAY = "Patient";
|
||||||
|
public static final String CS_OBJECT_ROLE_4_DOMAIN_RESOURCE = "4";
|
||||||
|
public static final String CS_OBJECT_ROLE_4_DOMAIN_RESOURCE_DISPLAY = "Domain Resource";
|
||||||
|
public static final String CS_RESTFUL_INTERACTION = "http://hl7.org/fhir/restful-interaction";
|
||||||
|
public static final String CS_OBJECT_ROLE_24_QUERY = "24";
|
||||||
|
static final String CS_OBJECT_ROLE_24_QUERY_DISPLAY = "Query";
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import org.hl7.fhir.r4.model.AuditEvent;
|
||||||
|
import org.hl7.fhir.r4.model.Coding;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public enum BalpProfileEnum {
|
||||||
|
BASIC_CREATE(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Create",
|
||||||
|
AuditEvent.AuditEventAction.C,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110153", "Source Role ID"),
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110152", "Destination Role ID")
|
||||||
|
),
|
||||||
|
PATIENT_CREATE(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientCreate",
|
||||||
|
AuditEvent.AuditEventAction.C,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110153", "Source Role ID"),
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110152", "Destination Role ID")
|
||||||
|
),
|
||||||
|
|
||||||
|
BASIC_UPDATE(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Update",
|
||||||
|
AuditEvent.AuditEventAction.U,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110153", "Source Role ID"),
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110152", "Destination Role ID")
|
||||||
|
),
|
||||||
|
PATIENT_UPDATE(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientUpdate",
|
||||||
|
AuditEvent.AuditEventAction.U,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110153", "Source Role ID"),
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110152", "Destination Role ID")
|
||||||
|
),
|
||||||
|
|
||||||
|
BASIC_DELETE(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Delete",
|
||||||
|
AuditEvent.AuditEventAction.D,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110150", "Application"),
|
||||||
|
() -> new Coding("http://terminology.hl7.org/CodeSystem/provenance-participant-type", "custodian", "Custodian")
|
||||||
|
),
|
||||||
|
PATIENT_DELETE(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientDelete",
|
||||||
|
AuditEvent.AuditEventAction.D,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110150", "Application"),
|
||||||
|
() -> new Coding("http://terminology.hl7.org/CodeSystem/provenance-participant-type", "custodian", "Custodian")
|
||||||
|
),
|
||||||
|
|
||||||
|
BASIC_READ(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Read",
|
||||||
|
AuditEvent.AuditEventAction.R,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110153", "Source Role ID"),
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110152", "Destination Role ID")
|
||||||
|
),
|
||||||
|
PATIENT_READ(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientRead",
|
||||||
|
AuditEvent.AuditEventAction.R,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110153", "Source Role ID"),
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110152", "Destination Role ID")
|
||||||
|
),
|
||||||
|
|
||||||
|
BASIC_QUERY(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Query",
|
||||||
|
AuditEvent.AuditEventAction.E,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110153", "Source Role ID"),
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110152", "Destination Role ID")
|
||||||
|
),
|
||||||
|
PATIENT_QUERY(
|
||||||
|
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientQuery",
|
||||||
|
AuditEvent.AuditEventAction.E,
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110153", "Source Role ID"),
|
||||||
|
() -> new Coding("http://dicom.nema.org/resources/ontology/DCM", "110152", "Destination Role ID")
|
||||||
|
),
|
||||||
|
|
||||||
|
;
|
||||||
|
private final String myProfileUrl;
|
||||||
|
private final AuditEvent.AuditEventAction myAction;
|
||||||
|
private final Supplier<Coding> myAgentClientTypeCoding;
|
||||||
|
private final Supplier<Coding> myAgentServerTypeCoding;
|
||||||
|
|
||||||
|
BalpProfileEnum(String theProfileUrl, AuditEvent.AuditEventAction theAction, Supplier<Coding> theAgentClientTypeCoding, Supplier<Coding> theAgentServerTypeCoding) {
|
||||||
|
myProfileUrl = theProfileUrl;
|
||||||
|
myAction = theAction;
|
||||||
|
myAgentClientTypeCoding = theAgentClientTypeCoding;
|
||||||
|
myAgentServerTypeCoding = theAgentServerTypeCoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Coding getAgentClientTypeCoding() {
|
||||||
|
return myAgentClientTypeCoding.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Coding getAgentServerTypeCoding() {
|
||||||
|
return myAgentServerTypeCoding.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProfileUrl() {
|
||||||
|
return myProfileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuditEvent.AuditEventAction getAction() {
|
||||||
|
return myAction;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||||
|
import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.r4.model.AuditEvent;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FhirClientBalpSink implements IBalpAuditEventSink {
|
||||||
|
|
||||||
|
private final IGenericClient myClient;
|
||||||
|
private final VersionCanonicalizer myVersionCanonicalizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the FhirContext to use when initiating outgoing connections
|
||||||
|
*
|
||||||
|
* @param theFhirContext The FhirContext instance. This context must be
|
||||||
|
* for the FHIR Version supported by the target/sink
|
||||||
|
* server (as opposed to the FHIR Version supported
|
||||||
|
* by the audit source).
|
||||||
|
* @param theTargetBaseUrl The FHIR server base URL for the target/sink server to
|
||||||
|
* receive audit events.
|
||||||
|
*/
|
||||||
|
public FhirClientBalpSink(@Nonnull FhirContext theFhirContext, @Nonnull String theTargetBaseUrl) {
|
||||||
|
this(theFhirContext, theTargetBaseUrl, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the FhirContext to use when initiating outgoing connections
|
||||||
|
*
|
||||||
|
* @param theFhirContext The FhirContext instance. This context must be
|
||||||
|
* for the FHIR Version supported by the target/sink
|
||||||
|
* server (as opposed to the FHIR Version supported
|
||||||
|
* by the audit source).
|
||||||
|
* @param theTargetBaseUrl The FHIR server base URL for the target/sink server to
|
||||||
|
* receive audit events.
|
||||||
|
* @param theClientInterceptors An optional list of interceptors to register against
|
||||||
|
* the client. May be {@literal null}.
|
||||||
|
*/
|
||||||
|
public FhirClientBalpSink(@Nonnull FhirContext theFhirContext, @Nonnull String theTargetBaseUrl, @Nullable List<Object> theClientInterceptors) {
|
||||||
|
this(createClient(theFhirContext, theTargetBaseUrl, theClientInterceptors));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param theClient The FHIR client to use as a sink.
|
||||||
|
*/
|
||||||
|
public FhirClientBalpSink(IGenericClient theClient) {
|
||||||
|
myClient = theClient;
|
||||||
|
myVersionCanonicalizer = new VersionCanonicalizer(myClient.getFhirContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void recordAuditEvent(AuditEvent theAuditEvent) {
|
||||||
|
IBaseResource auditEvent = myVersionCanonicalizer.auditEventFromCanonical(theAuditEvent);
|
||||||
|
recordAuditEvent(auditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void recordAuditEvent(IBaseResource auditEvent) {
|
||||||
|
transmitEventToClient(auditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void transmitEventToClient(IBaseResource auditEvent) {
|
||||||
|
myClient
|
||||||
|
.create()
|
||||||
|
.resource(auditEvent)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
static IGenericClient createClient(@Nonnull FhirContext theFhirContext, @Nonnull String theTargetBaseUrl, @Nullable List<Object> theClientInterceptors) {
|
||||||
|
Validate.notNull(theFhirContext, "theFhirContext must not be null");
|
||||||
|
Validate.notBlank(theTargetBaseUrl, "theTargetBaseUrl must not be null or blank");
|
||||||
|
IGenericClient client = theFhirContext.newRestfulGenericClient(theTargetBaseUrl);
|
||||||
|
if (theClientInterceptors != null) {
|
||||||
|
theClientInterceptors.forEach(client::registerInterceptor);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r4.model.AuditEvent;
|
||||||
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface is intended to be implemented in order to supply implementation
|
||||||
|
* strategy details to the {@link BalpAuditCaptureInterceptor}.
|
||||||
|
*
|
||||||
|
* @since 6.6.0
|
||||||
|
*/
|
||||||
|
public interface IBalpAuditContextServices {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a Reference to the client that was used to
|
||||||
|
* perform the action in question.
|
||||||
|
*
|
||||||
|
* @param theRequestDetails The request details object
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
Reference getAgentClientWho(RequestDetails theRequestDetails);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a Reference to the user that was used to
|
||||||
|
* perform the action in question.
|
||||||
|
*
|
||||||
|
* @param theRequestDetails The request details object
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
Reference getAgentUserWho(RequestDetails theRequestDetails);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the requesting network address to include in the AuditEvent.
|
||||||
|
*
|
||||||
|
* @see #getNetworkAddressType(RequestDetails) If this method is returning an adress type that is not
|
||||||
|
* an IP address, you must also oerride this method and return the correct code.
|
||||||
|
*/
|
||||||
|
default String getNetworkAddress(RequestDetails theRequestDetails) {
|
||||||
|
String remoteAddr = null;
|
||||||
|
if (theRequestDetails instanceof ServletRequestDetails) {
|
||||||
|
remoteAddr = ((ServletRequestDetails) theRequestDetails).getServletRequest().getRemoteAddr();
|
||||||
|
}
|
||||||
|
return remoteAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a code representing the appropriate return type for {@link #getNetworkAddress(RequestDetails)}. The
|
||||||
|
* default implementation returns {@link BalpConstants#AUDIT_EVENT_AGENT_NETWORK_TYPE_IP_ADDRESS}.
|
||||||
|
*
|
||||||
|
* @param theRequestDetails The request details object
|
||||||
|
* @see #getNetworkAddress(RequestDetails)
|
||||||
|
* @see BalpConstants#AUDIT_EVENT_AGENT_NETWORK_TYPE_MACHINE_NAME Potential return type for this method
|
||||||
|
* @see BalpConstants#AUDIT_EVENT_AGENT_NETWORK_TYPE_IP_ADDRESS Potential return type for this method
|
||||||
|
* @see BalpConstants#AUDIT_EVENT_AGENT_NETWORK_TYPE_URI Potential return type for this method
|
||||||
|
*/
|
||||||
|
default AuditEvent.AuditEventAgentNetworkType getNetworkAddressType(RequestDetails theRequestDetails) {
|
||||||
|
return BalpConstants.AUDIT_EVENT_AGENT_NETWORK_TYPE_IP_ADDRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns an entity resource ID from an {@link IIdType} to a String.
|
||||||
|
* The default implementation injects the server's base URL into
|
||||||
|
* the ID in order to create fully qualified URLs for resource
|
||||||
|
* references within BALP events.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
default String massageResourceIdForStorage(@Nonnull RequestDetails theRequestDetails, @Nonnull IBaseResource theResource, @Nonnull IIdType theResourceId) {
|
||||||
|
String serverBaseUrl = theRequestDetails.getServerBaseForRequest();
|
||||||
|
String resourceName = theResourceId.getResourceType();
|
||||||
|
return theResourceId.withServerBase(serverBaseUrl, resourceName).getValue();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import org.hl7.fhir.r4.model.AuditEvent;
|
||||||
|
|
||||||
|
public interface IBalpAuditEventSink {
|
||||||
|
|
||||||
|
void recordAuditEvent(AuditEvent theAuditEvent);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
|
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||||
|
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
||||||
|
import ca.uhn.fhir.interceptor.api.IPointcut;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.AsyncMemoryQueueBackedFhirClientBalpSink;
|
||||||
|
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension;
|
||||||
|
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||||
|
import org.hl7.fhir.dstu3.model.AuditEvent;
|
||||||
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.awaitility.Awaitility.await;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.lessThan;
|
||||||
|
|
||||||
|
public class AsyncMemoryQueueBackedFhirClientBalpSinkTest {
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(0)
|
||||||
|
private RestfulServerExtension myServer = new RestfulServerExtension(FhirVersionEnum.DSTU3);
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(1)
|
||||||
|
private HashMapResourceProviderExtension<AuditEvent> myAuditEventProvider = new HashMapResourceProviderExtension<>(myServer, AuditEvent.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void recordAuditEvent() {
|
||||||
|
// Setup
|
||||||
|
AsyncMemoryQueueBackedFhirClientBalpSink sink = new AsyncMemoryQueueBackedFhirClientBalpSink(myServer.getFhirContext(), myServer.getBaseUrl());
|
||||||
|
sink.start();
|
||||||
|
try {
|
||||||
|
org.hl7.fhir.r4.model.AuditEvent auditEvent1 = new org.hl7.fhir.r4.model.AuditEvent();
|
||||||
|
auditEvent1.addEntity().setWhat(new Reference("Patient/123"));
|
||||||
|
org.hl7.fhir.r4.model.AuditEvent auditEvent2 = new org.hl7.fhir.r4.model.AuditEvent();
|
||||||
|
auditEvent2.addEntity().setWhat(new Reference("Patient/456"));
|
||||||
|
|
||||||
|
// Test
|
||||||
|
sink.recordAuditEvent(auditEvent1);
|
||||||
|
sink.recordAuditEvent(auditEvent2);
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
myAuditEventProvider.waitForCreateCount(2);
|
||||||
|
List<String> whats = myAuditEventProvider
|
||||||
|
.getStoredResources()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getEntity().get(0).getReference().getReference())
|
||||||
|
.toList();
|
||||||
|
assertThat(whats, containsInAnyOrder("Patient/123", "Patient/456"));
|
||||||
|
} finally {
|
||||||
|
sink.stop();
|
||||||
|
await().until(sink::isRunning, equalTo(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void recordAuditEvent_AutoRetry() {
|
||||||
|
// Setup
|
||||||
|
AsyncMemoryQueueBackedFhirClientBalpSink sink = new AsyncMemoryQueueBackedFhirClientBalpSink(myServer.getFhirContext(), myServer.getBaseUrl());
|
||||||
|
sink.start();
|
||||||
|
try {
|
||||||
|
org.hl7.fhir.r4.model.AuditEvent auditEvent1 = new org.hl7.fhir.r4.model.AuditEvent();
|
||||||
|
auditEvent1.addEntity().setWhat(new Reference("Patient/123"));
|
||||||
|
org.hl7.fhir.r4.model.AuditEvent auditEvent2 = new org.hl7.fhir.r4.model.AuditEvent();
|
||||||
|
auditEvent2.addEntity().setWhat(new Reference("Patient/456"));
|
||||||
|
|
||||||
|
AtomicInteger counter = new AtomicInteger(10);
|
||||||
|
myServer.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, new IAnonymousInterceptor() {
|
||||||
|
@Override
|
||||||
|
public void invoke(IPointcut thePointcut, HookParams theArgs) {
|
||||||
|
if (counter.decrementAndGet() > 0) {
|
||||||
|
throw new InternalErrorException("Intentional error for unit test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test
|
||||||
|
sink.recordAuditEvent(auditEvent1);
|
||||||
|
sink.recordAuditEvent(auditEvent2);
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
myAuditEventProvider.waitForCreateCount(2);
|
||||||
|
assertThat(counter.get(), lessThan(1));
|
||||||
|
List<String> whats = myAuditEventProvider
|
||||||
|
.getStoredResources()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getEntity().get(0).getReference().getReference())
|
||||||
|
.toList();
|
||||||
|
assertThat(whats.toString(), whats, containsInAnyOrder("Patient/123", "Patient/456"));
|
||||||
|
} finally {
|
||||||
|
sink.stop();
|
||||||
|
await().until(sink::isRunning, equalTo(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,858 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
|
||||||
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
|
import ca.uhn.fhir.rest.api.SearchStyleEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||||
|
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
||||||
|
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
|
||||||
|
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||||
|
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension;
|
||||||
|
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||||
|
import ca.uhn.fhir.validation.FhirValidator;
|
||||||
|
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||||
|
import ca.uhn.fhir.validation.SingleValidationMessage;
|
||||||
|
import ca.uhn.fhir.validation.ValidationResult;
|
||||||
|
import org.hl7.fhir.common.hapi.validation.support.*;
|
||||||
|
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r4.model.*;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.storage.interceptor.balp.BalpConstants.*;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.contains;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class BalpAuditCaptureInterceptorTest implements ITestDataBuilder {
|
||||||
|
|
||||||
|
private static final FhirContext ourCtx = FhirContext.forR4Cached();
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(0)
|
||||||
|
public static final RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
|
||||||
|
.withPagingProvider(new FifoMemoryPagingProvider(10))
|
||||||
|
.keepAliveBetweenTests();
|
||||||
|
private static FhirValidator ourValidator;
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(1)
|
||||||
|
public final HashMapResourceProviderExtension<Patient> myPatientProvider = new HashMapResourceProviderExtension<>(ourServer, Patient.class);
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(1)
|
||||||
|
public final HashMapResourceProviderExtension<Observation> myObservationProvider = new HashMapResourceProviderExtension<>(ourServer, Observation.class);
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(1)
|
||||||
|
public final HashMapResourceProviderExtension<CodeSystem> myCodeSystemProvider = new HashMapResourceProviderExtension<>(ourServer, CodeSystem.class);
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(1)
|
||||||
|
public final HashMapResourceProviderExtension<ListResource> myListProvider = new HashMapResourceProviderExtension<>(ourServer, ListResource.class);
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IBalpAuditEventSink myAuditEventSink;
|
||||||
|
@Mock(strictness = Mock.Strictness.LENIENT)
|
||||||
|
private IBalpAuditContextServices myContextServices;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<AuditEvent> myAuditEventCaptor;
|
||||||
|
|
||||||
|
private BalpAuditCaptureInterceptor mySvc;
|
||||||
|
private IGenericClient myClient;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static List<String> getQueries(AuditEvent theAuditEvent) {
|
||||||
|
return theAuditEvent
|
||||||
|
.getEntity()
|
||||||
|
.stream()
|
||||||
|
.filter(t -> t.getType().getSystem().equals(CS_AUDIT_ENTITY_TYPE))
|
||||||
|
.filter(t -> t.getType().getCode().equals(CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT))
|
||||||
|
.filter(t -> t.getRole().getSystem().equals(CS_OBJECT_ROLE))
|
||||||
|
.filter(t -> t.getRole().getCode().equals(CS_OBJECT_ROLE_24_QUERY))
|
||||||
|
.map(t -> new String(t.getQuery(), StandardCharsets.UTF_8))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static List<String> getDescriptions(AuditEvent theAuditEvent) {
|
||||||
|
return theAuditEvent
|
||||||
|
.getEntity()
|
||||||
|
.stream()
|
||||||
|
.filter(t -> t.getType().getSystem().equals(CS_AUDIT_ENTITY_TYPE))
|
||||||
|
.filter(t -> t.getType().getCode().equals(CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT))
|
||||||
|
.filter(t -> t.getRole().getSystem().equals(CS_OBJECT_ROLE))
|
||||||
|
.filter(t -> t.getRole().getCode().equals(CS_OBJECT_ROLE_24_QUERY))
|
||||||
|
.map(AuditEvent.AuditEventEntityComponent::getDescription)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertType(AuditEvent theAuditEvent) {
|
||||||
|
assertEquals(CS_AUDIT_EVENT_TYPE, theAuditEvent.getType().getSystem());
|
||||||
|
assertEquals("rest", theAuditEvent.getType().getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSubType(AuditEvent theAuditEvent, String theSubType) {
|
||||||
|
assertEquals(CS_RESTFUL_INTERACTION, theAuditEvent.getSubtypeFirstRep().getSystem());
|
||||||
|
assertEquals(theSubType, theAuditEvent.getSubtypeFirstRep().getCode());
|
||||||
|
assertEquals(1, theAuditEvent.getSubtype().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertAuditEventValidatesAgainstBalpProfile(AuditEvent auditEvent) {
|
||||||
|
ValidationResult outcome = ourValidator.validateWithResult(auditEvent);
|
||||||
|
ourLog.info("Validation outcome: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.toOperationOutcome()));
|
||||||
|
|
||||||
|
List<SingleValidationMessage> issues = outcome
|
||||||
|
.getMessages()
|
||||||
|
.stream()
|
||||||
|
.filter(t -> t.getSeverity().ordinal() >= ResultSeverityEnum.WARNING.ordinal())
|
||||||
|
.toList();
|
||||||
|
if (!issues.isEmpty()) {
|
||||||
|
fail("Issues:\n * " + issues.stream().map(SingleValidationMessage::toString).collect(Collectors.joining("\n * ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertHasSystemObjectEntities(AuditEvent theAuditEvent, String... theResourceIds) {
|
||||||
|
List<String> systemObjects = theAuditEvent
|
||||||
|
.getEntity()
|
||||||
|
.stream()
|
||||||
|
.filter(t -> t.getType().getSystem().equals(CS_AUDIT_ENTITY_TYPE))
|
||||||
|
.filter(t -> t.getType().getCode().equals(CS_AUDIT_ENTITY_TYPE_2_SYSTEM_OBJECT))
|
||||||
|
.filter(t -> t.getRole().getSystem().equals(CS_OBJECT_ROLE))
|
||||||
|
.filter(t -> t.getRole().getCode().equals(CS_OBJECT_ROLE_4_DOMAIN_RESOURCE))
|
||||||
|
.map(t -> t.getWhat().getReference())
|
||||||
|
.toList();
|
||||||
|
assertThat(Arrays.asList(theResourceIds).toString(), systemObjects, containsInAnyOrder(theResourceIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertHasPatientEntities(AuditEvent theAuditEvent, String... theResourceIds) {
|
||||||
|
List<String> patients = theAuditEvent
|
||||||
|
.getEntity()
|
||||||
|
.stream()
|
||||||
|
.filter(t -> t.getType().getSystem().equals(CS_AUDIT_ENTITY_TYPE))
|
||||||
|
.filter(t -> t.getType().getCode().equals(CS_AUDIT_ENTITY_TYPE_1_PERSON))
|
||||||
|
.filter(t -> t.getRole().getSystem().equals(CS_OBJECT_ROLE))
|
||||||
|
.filter(t -> t.getRole().getCode().equals(CS_OBJECT_ROLE_1_PATIENT))
|
||||||
|
.map(t -> t.getWhat().getReference())
|
||||||
|
.map(t -> new IdType(t).toUnqualified().getValue())
|
||||||
|
.toList();
|
||||||
|
assertThat(patients.toString(), patients, containsInAnyOrder(theResourceIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void beforeAll() throws IOException {
|
||||||
|
NpmPackageValidationSupport npmPackageSupport = new NpmPackageValidationSupport(ourCtx);
|
||||||
|
npmPackageSupport.loadPackageFromClasspath("classpath:balp/balp-1.1.1.tgz");
|
||||||
|
|
||||||
|
ValidationSupportChain validationSupportChain = new ValidationSupportChain(
|
||||||
|
npmPackageSupport,
|
||||||
|
new DefaultProfileValidationSupport(ourCtx),
|
||||||
|
new CommonCodeSystemsTerminologyService(ourCtx),
|
||||||
|
new InMemoryTerminologyServerValidationSupport(ourCtx),
|
||||||
|
new SnapshotGeneratingValidationSupport(ourCtx)
|
||||||
|
);
|
||||||
|
CachingValidationSupport validationSupport = new CachingValidationSupport(validationSupportChain);
|
||||||
|
|
||||||
|
ourValidator = ourCtx.newValidator();
|
||||||
|
FhirInstanceValidator validator = new FhirInstanceValidator(validationSupport);
|
||||||
|
validator.setNoExtensibleWarnings(true);
|
||||||
|
ourValidator.registerValidatorModule(validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void before() {
|
||||||
|
ourServer.getInterceptorService().unregisterInterceptorsIf(t -> t instanceof BalpAuditCaptureInterceptor);
|
||||||
|
myClient = ourServer.getFhirClient();
|
||||||
|
myClient.capabilities().ofType(CapabilityStatement.class).execute(); // pre-validate this
|
||||||
|
myClient.registerInterceptor(new LoggingInterceptor(false));
|
||||||
|
|
||||||
|
when(myContextServices.getAgentClientWho(any())).thenReturn(new Reference().setIdentifier(new Identifier().setSystem("http://clients").setValue("123")));
|
||||||
|
when(myContextServices.getAgentUserWho(any())).thenReturn(new Reference().setIdentifier(new Identifier().setSystem("http://users").setValue("abc")));
|
||||||
|
when(myContextServices.massageResourceIdForStorage(any(), any(), any())).thenCallRealMethod();
|
||||||
|
when(myContextServices.getNetworkAddressType(any())).thenCallRealMethod();
|
||||||
|
|
||||||
|
mySvc = new BalpAuditCaptureInterceptor(myAuditEventSink, myContextServices);
|
||||||
|
ourServer.registerInterceptor(mySvc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateCodeSystem() {
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
CodeSystem cs = new CodeSystem();
|
||||||
|
cs.setUrl("http://foo");
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.create()
|
||||||
|
.resource(cs)
|
||||||
|
.execute();
|
||||||
|
IIdType csId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertTrue(outcome.getCreated());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.BASIC_CREATE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "create");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.C, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, csId.getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateObservation() {
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
Observation obs = buildResource("Observation", withSubject("Patient/P1"));
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.create()
|
||||||
|
.resource(obs)
|
||||||
|
.execute();
|
||||||
|
IIdType obsId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertTrue(outcome.getCreated());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_CREATE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "create");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.C, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, obsId.getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreatePatient() {
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
Patient p = buildResource("Patient", withId("P1"), withFamily("Simpson"), withGiven("Homer"));
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.create()
|
||||||
|
.resource(p)
|
||||||
|
.execute();
|
||||||
|
IIdType patientId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertTrue(outcome.getCreated());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_CREATE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "create");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.C, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, patientId.getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, patientId.toUnqualified().getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeleteCodeSystem() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
CodeSystem cs = new CodeSystem();
|
||||||
|
cs.setUrl("http://foo");
|
||||||
|
myCodeSystemProvider.store(cs);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
cs.setUrl("http://foo2");
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.delete()
|
||||||
|
.resource(cs)
|
||||||
|
.execute();
|
||||||
|
IIdType csId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.BASIC_DELETE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "delete");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.D, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, csId.withVersion("1").getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeleteObservation() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
Observation obs = buildResource("Observation", withSubject("Patient/P1"));
|
||||||
|
myObservationProvider.store(obs);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
obs.setStatus(Observation.ObservationStatus.FINAL);
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.delete()
|
||||||
|
.resource(obs)
|
||||||
|
.execute();
|
||||||
|
IIdType obsId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_DELETE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "delete");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.D, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, obsId.withVersion("1").getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeletePatient() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
Patient p = buildResource("Patient", withId("P1"), withFamily("Simpson"), withGiven("Homer"));
|
||||||
|
myPatientProvider.store(p);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
p.setActive(false);
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.delete()
|
||||||
|
.resource(p)
|
||||||
|
.execute();
|
||||||
|
IIdType patientId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_DELETE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "delete");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.D, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, patientId.withVersion("1").getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, patientId.toUnqualified().withVersion("1").getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadPatient() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
createPatient(withId("P1"), withFamily("Simpson"), withGiven("Homer"));
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
Patient patient = myClient
|
||||||
|
.read()
|
||||||
|
.resource(Patient.class)
|
||||||
|
.withId("P1")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals("Simpson", patient.getNameFirstRep().getFamily());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_READ);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "read");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.R, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, patient.getId());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, patient.getIdElement().toUnqualified().getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadResourceNotInPatientCompartment() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
CodeSystem cs = new CodeSystem();
|
||||||
|
cs.setUrl("http://foo");
|
||||||
|
IIdType csId = myCodeSystemProvider.store(cs);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
CodeSystem actual = myClient
|
||||||
|
.read()
|
||||||
|
.resource(CodeSystem.class)
|
||||||
|
.withId(csId.toUnqualifiedVersionless())
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals("http://foo", actual.getUrl());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.BASIC_READ);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "read");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.R, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, ourServer.getBaseUrl() + "/" + csId.getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadResourceInPatientCompartment_WithOneSubject() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
createObservation(withId("O1"), withSubject("Patient/P1"));
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
Observation observation = myClient
|
||||||
|
.read()
|
||||||
|
.resource(Observation.class)
|
||||||
|
.withId("O1")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals("Patient/P1", observation.getSubject().getReference());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "read");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.R, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, observation.getId());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadResourceInPatientCompartment_WithTwoSubjects_VRead() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
ListResource list = new ListResource();
|
||||||
|
list.addEntry().getItem().setReference("Patient/P1");
|
||||||
|
list.addEntry().getItem().setReference("Patient/P2");
|
||||||
|
IIdType listId = myListProvider.store(list);
|
||||||
|
|
||||||
|
mySvc.setAdditionalPatientCompartmentParamNames(Set.of("item"));
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
ListResource outcome = myClient
|
||||||
|
.read()
|
||||||
|
.resource(ListResource.class)
|
||||||
|
.withId(listId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals(2, outcome.getEntry().size());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(2)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getAllValues().get(0);
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "vread");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.R, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, ourServer.getBaseUrl() + "/" + listId.getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearch_ResponseIncludesSinglePatientCompartment() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
create10Observations("Patient/P1");
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
Bundle outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource(Observation.class)
|
||||||
|
.where(Observation.SUBJECT.hasId("Patient/P1"))
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals(10, outcome.getEntry().size());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_QUERY);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "search-type");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.E, auditEvent.getAction());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
assertQuery(auditEvent, ourServer.getBaseUrl() + "/Observation?subject=Patient%2FP1");
|
||||||
|
assertQueryDescription(auditEvent, "GET /Observation?subject=Patient%2FP1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearch_NoPatientCompartmentResources() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
CodeSystem cs = new CodeSystem();
|
||||||
|
cs.setUrl("http://cs" + i);
|
||||||
|
myCodeSystemProvider.store(cs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
Bundle outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource(CodeSystem.class)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals(5, outcome.getEntry().size());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.BASIC_QUERY);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "search-type");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.E, auditEvent.getAction());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent);
|
||||||
|
assertQuery(auditEvent, ourServer.getBaseUrl() + "/CodeSystem");
|
||||||
|
assertQueryDescription(auditEvent, "GET /CodeSystem");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearch_ResponseIncludesSinglePatientCompartment_LoadPageTwo() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
create10Observations("Patient/P1");
|
||||||
|
Bundle outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource(Observation.class)
|
||||||
|
.where(Observation.SUBJECT.hasId("Patient/P1"))
|
||||||
|
.count(5)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
outcome = myClient
|
||||||
|
.loadPage()
|
||||||
|
.next(outcome)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals(5, outcome.getEntry().size());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(2)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
verifyNoMoreInteractions(myAuditEventSink);
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getAllValues().get(0);
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_QUERY);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "search-type");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.E, auditEvent.getAction());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
assertQuery(auditEvent, ourServer.getBaseUrl() + "/Observation?_count=5&subject=Patient%2FP1");
|
||||||
|
assertQueryDescription(auditEvent, "GET /Observation?subject=Patient%2FP1&_count=5");
|
||||||
|
|
||||||
|
auditEvent = myAuditEventCaptor.getAllValues().get(1);
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_QUERY);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "search-type");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.E, auditEvent.getAction());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearch_ResponseIncludesSinglePatientCompartment_UsePost() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
create10Observations("Patient/P1");
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
Bundle outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource(Observation.class)
|
||||||
|
.where(Observation.SUBJECT.hasId("Patient/P1"))
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.usingStyle(SearchStyleEnum.POST)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals(10, outcome.getEntry().size());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_QUERY);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "search-type");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.E, auditEvent.getAction());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
assertQuery(auditEvent, ourServer.getBaseUrl() + "/Observation/_search?subject=Patient%2FP1");
|
||||||
|
assertQueryDescription(auditEvent, "POST /Observation/_search");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearch_ResponseIncludesSinglePatientCompartment_UseGetSearch() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
create10Observations("Patient/P1");
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
Bundle outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource(Observation.class)
|
||||||
|
.where(Observation.SUBJECT.hasId("Patient/P1"))
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.usingStyle(SearchStyleEnum.GET_WITH_SEARCH)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
assertEquals(10, outcome.getEntry().size());
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_QUERY);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "search-type");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.E, auditEvent.getAction());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
assertQuery(auditEvent, ourServer.getBaseUrl() + "/Observation/_search?subject=Patient%2FP1");
|
||||||
|
assertQueryDescription(auditEvent, "GET /Observation/_search?subject=Patient%2FP1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateCodeSystem() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
CodeSystem cs = new CodeSystem();
|
||||||
|
cs.setUrl("http://foo");
|
||||||
|
myCodeSystemProvider.store(cs);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
cs.setUrl("http://foo2");
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.update()
|
||||||
|
.resource(cs)
|
||||||
|
.execute();
|
||||||
|
IIdType csId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.BASIC_UPDATE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "update");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.U, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, csId.getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateObservation() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
Observation obs = buildResource("Observation", withSubject("Patient/P1"));
|
||||||
|
myObservationProvider.store(obs);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
obs.setStatus(Observation.ObservationStatus.FINAL);
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.update()
|
||||||
|
.resource(obs)
|
||||||
|
.execute();
|
||||||
|
IIdType obsId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_UPDATE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "update");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.U, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, obsId.getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, "Patient/P1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdatePatient() {
|
||||||
|
// Setup
|
||||||
|
|
||||||
|
Patient p = buildResource("Patient", withId("P1"), withFamily("Simpson"), withGiven("Homer"));
|
||||||
|
myPatientProvider.store(p);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
|
p.setActive(false);
|
||||||
|
MethodOutcome outcome = myClient
|
||||||
|
.update()
|
||||||
|
.resource(p)
|
||||||
|
.execute();
|
||||||
|
IIdType patientId = outcome.getId();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
|
||||||
|
verify(myAuditEventSink, times(1)).recordAuditEvent(myAuditEventCaptor.capture());
|
||||||
|
|
||||||
|
AuditEvent auditEvent = myAuditEventCaptor.getValue();
|
||||||
|
ourLog.info("Audit Event: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(auditEvent));
|
||||||
|
assertAuditEventValidatesAgainstBalpProfile(auditEvent);
|
||||||
|
assertHasProfile(auditEvent, BalpProfileEnum.PATIENT_UPDATE);
|
||||||
|
assertType(auditEvent);
|
||||||
|
assertSubType(auditEvent, "update");
|
||||||
|
assertEquals(AuditEvent.AuditEventAction.U, auditEvent.getAction());
|
||||||
|
assertHasSystemObjectEntities(auditEvent, patientId.getValue());
|
||||||
|
assertEquals(AuditEvent.AuditEventOutcome._0, auditEvent.getOutcome());
|
||||||
|
assertHasPatientEntities(auditEvent, patientId.toUnqualified().getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void create10Observations(String... thePatientIds) {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
createObservation(withId("O" + i), withSubject(thePatientIds[i % thePatientIds.length]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertQuery(AuditEvent theAuditEvent, String theQuery) {
|
||||||
|
List<String> queries = getQueries(theAuditEvent);
|
||||||
|
assertThat(queries, contains(theQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertQueryDescription(AuditEvent theAuditEvent, String theQuery) {
|
||||||
|
List<String> queries = getDescriptions(theAuditEvent);
|
||||||
|
assertThat(queries, contains(theQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertHasProfile(AuditEvent theAuditEvent, BalpProfileEnum theProfile) {
|
||||||
|
List<String> profiles = theAuditEvent
|
||||||
|
.getMeta()
|
||||||
|
.getProfile()
|
||||||
|
.stream()
|
||||||
|
.map(PrimitiveType::asStringValue)
|
||||||
|
.toList();
|
||||||
|
assertThat(profiles, contains(theProfile.getProfileUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IIdType doCreateResource(IBaseResource theResource) {
|
||||||
|
return getProvider(theResource).create(theResource, new SystemRequestDetails()).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IIdType doUpdateResource(IBaseResource theResource) {
|
||||||
|
return getProvider(theResource).update(theResource, null, new SystemRequestDetails()).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T extends IBaseResource> HashMapResourceProviderExtension<T> getProvider(T theResource) {
|
||||||
|
return switch (ourCtx.getResourceType(theResource)) {
|
||||||
|
case "Patient" -> (HashMapResourceProviderExtension<T>) myPatientProvider;
|
||||||
|
case "Observation" -> (HashMapResourceProviderExtension<T>) myObservationProvider;
|
||||||
|
case "CodeSystem" -> (HashMapResourceProviderExtension<T>) myCodeSystemProvider;
|
||||||
|
default -> throw new IllegalArgumentException("Unable to handle: " + theResource);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FhirContext getFhirContext() {
|
||||||
|
return ourCtx;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
|
import ca.uhn.fhir.storage.interceptor.balp.FhirClientBalpSink;
|
||||||
|
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension;
|
||||||
|
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||||
|
import org.hl7.fhir.dstu3.model.AuditEvent;
|
||||||
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
|
||||||
|
public class FhirClientBalpSinkTest {
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(0)
|
||||||
|
private RestfulServerExtension myServer = new RestfulServerExtension(FhirVersionEnum.DSTU3);
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(1)
|
||||||
|
private HashMapResourceProviderExtension<AuditEvent> myAuditEventProvider = new HashMapResourceProviderExtension<>(myServer, AuditEvent.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void recordAuditEvent() {
|
||||||
|
// Setup
|
||||||
|
FhirClientBalpSink sink = new FhirClientBalpSink(myServer.getFhirContext(), myServer.getBaseUrl());
|
||||||
|
org.hl7.fhir.r4.model.AuditEvent auditEvent1 = new org.hl7.fhir.r4.model.AuditEvent();
|
||||||
|
auditEvent1.addEntity().setWhat(new Reference("Patient/123"));
|
||||||
|
org.hl7.fhir.r4.model.AuditEvent auditEvent2 = new org.hl7.fhir.r4.model.AuditEvent();
|
||||||
|
auditEvent2.addEntity().setWhat(new Reference("Patient/456"));
|
||||||
|
|
||||||
|
// Test
|
||||||
|
sink.recordAuditEvent(auditEvent1);
|
||||||
|
sink.recordAuditEvent(auditEvent2);
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
myAuditEventProvider.waitForCreateCount(2);
|
||||||
|
List<String> whats = myAuditEventProvider
|
||||||
|
.getStoredResources()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getEntity().get(0).getReference().getReference())
|
||||||
|
.toList();
|
||||||
|
assertThat(whats, containsInAnyOrder("Patient/123", "Patient/456"));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Issues for discussion:
|
||||||
|
* <li>
|
||||||
|
* In the BasicAudit.PatientRead profile, if the object being read is something
|
||||||
|
* in the patient compartment but not the patient (e.g. Observation) is this correct:
|
||||||
|
* the entity:data is the Observation ID, but the entity:patient is the Patient ID
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* In the BasicAudit.PatientRead profile, what if the resource being read is
|
||||||
|
* a List with multiple patients referenced?
|
||||||
|
* Implementation currently creates multiple auditevent resources
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* In the BasicAudit.PatientQuery profile, what if the search matched resources
|
||||||
|
* belonging to different patients? E.g. The user may have requested
|
||||||
|
* Observation?patient=123 but an MDM might implicitly widen that to include
|
||||||
|
* Patient/456 (or the client even explicitly request this behavior through
|
||||||
|
* additional parameters). Currently adding multiple entity:patient repetitions
|
||||||
|
* in a single auditevent to handle this case.
|
||||||
|
* </li>
|
||||||
|
*/
|
||||||
|
package ca.uhn.fhir.storage.interceptor.balp;
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,17 @@
|
||||||
|
<configuration>
|
||||||
|
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>INFO</level>
|
||||||
|
</filter>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
|
@ -523,7 +523,7 @@ public class SearchNarrowingWithConsentAndAuthInterceptorTest {
|
||||||
|
|
||||||
@Search(allowUnknownParams = true)
|
@Search(allowUnknownParams = true)
|
||||||
@Override
|
@Override
|
||||||
public synchronized List<IBaseResource> searchAll(RequestDetails theRequestDetails) {
|
public synchronized IBundleProvider searchAll(RequestDetails theRequestDetails) {
|
||||||
myRequestParams.add(theRequestDetails.getParameters());
|
myRequestParams.add(theRequestDetails.getParameters());
|
||||||
return super.searchAll(theRequestDetails);
|
return super.searchAll(theRequestDetails);
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,10 +236,10 @@ public interface ITestDataBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default IBaseResource buildResource(String theResourceType, Consumer<IBaseResource>... theModifiers) {
|
default <T extends IBaseResource> T buildResource(String theResourceType, Consumer<IBaseResource>... theModifiers) {
|
||||||
IBaseResource resource = getFhirContext().getResourceDefinition(theResourceType).newInstance();
|
IBaseResource resource = getFhirContext().getResourceDefinition(theResourceType).newInstance();
|
||||||
applyElementModifiers(resource, theModifiers);
|
applyElementModifiers(resource, theModifiers);
|
||||||
return resource;
|
return (T) resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -253,6 +253,7 @@ public abstract class BaseJettyServerExtension<T extends BaseJettyServerExtensio
|
||||||
* Returns the server base URL with no trailing slash
|
* Returns the server base URL with no trailing slash
|
||||||
*/
|
*/
|
||||||
public String getBaseUrl() {
|
public String getBaseUrl() {
|
||||||
|
assert myServletPath.endsWith("/*");
|
||||||
return "http://localhost:" + myPort + myContextPath + myServletPath.substring(0, myServletPath.length() - 2);
|
return "http://localhost:" + myPort + myContextPath + myServletPath.substring(0, myServletPath.length() - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
*/
|
*/
|
||||||
package ca.uhn.fhir.test.utilities.server;
|
package ca.uhn.fhir.test.utilities.server;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.rest.annotation.Search;
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
|
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
@ -58,6 +60,12 @@ public class HashMapResourceProviderExtension<T extends IBaseResource> extends H
|
||||||
myRestfulServerExtension.getRestfulServer().unregisterProvider(HashMapResourceProviderExtension.this);
|
myRestfulServerExtension.getRestfulServer().unregisterProvider(HashMapResourceProviderExtension.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Search(allowUnknownParams = true)
|
||||||
|
public synchronized IBundleProvider searchAll(RequestDetails theRequestDetails) {
|
||||||
|
return super.searchAll(theRequestDetails);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void clear() {
|
public synchronized void clear() {
|
||||||
super.clear();
|
super.clear();
|
||||||
|
|
Loading…
Reference in New Issue