Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue-3455 - adding resource info from location when return preferenc… #4130

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* (C) Copyright IBM Corp. 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.linuxforhealth.fhir.server.spi.operation;


/**
* This class is used to represent a Resource context information.
*/
public class FHIRResourceContext {

private String resourceType;

private String id;

private String versionId;



/**
* @param resourceType
* @param id
* @param versionId
*/
public FHIRResourceContext(String resourceType, String id, String versionId) {
super();
this.resourceType = resourceType;
this.id = id;
this.versionId = versionId;
}



/**
* @return the resourceType
*/
public String getResourceType() {
return resourceType;
}


/**
* @param resourceType the resourceType to set
*/
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}


/**
* @return the id
*/
public String getId() {
return id;
}


/**
* @param id the id to set
*/
public void setId(String id) {
this.id = id;
}


/**
* @return the versionId
*/
public String getVersionId() {
return versionId;
}


/**
* @param versionId the versionId to set
*/
public void setVersionId(String versionId) {
this.versionId = versionId;
}
}
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
package org.linuxforhealth.fhir.server.spi.operation;

import java.net.URI;
import java.util.List;

import javax.ws.rs.core.Response;

@@ -24,6 +25,7 @@ public class FHIRRestOperationResponse {
private Resource prevResource;
private OperationOutcome operationOutcome;
private boolean deleted;
private List<FHIRResourceContext> fhirResourceContexts;

// For delete we need to return the version of the deletion marker
private int versionForETag;
@@ -155,4 +157,22 @@ public int getVersionForETag() {
public void setVersionForETag(int versionForETag) {
this.versionForETag = versionForETag;
}


/**
* @return the fhirResourceContexts
*/
public List<FHIRResourceContext> getFhirResourceContexts() {
return fhirResourceContexts;
}


/**
* @param fhirResourceContexts the fhirResourceContexts to set
*/
public void setFhirResourceContexts(List<FHIRResourceContext> fhirResourceContexts) {
this.fhirResourceContexts = fhirResourceContexts;
}


}
Original file line number Diff line number Diff line change
@@ -17,7 +17,6 @@
import java.io.ByteArrayInputStream;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
@@ -46,7 +45,6 @@
import org.linuxforhealth.fhir.client.FHIRRequestHeader;
import org.linuxforhealth.fhir.client.FHIRResponse;
import org.linuxforhealth.fhir.config.ConfigurationService;
import org.linuxforhealth.fhir.config.FHIRConfigHelper;
import org.linuxforhealth.fhir.config.FHIRConfiguration;
import org.linuxforhealth.fhir.config.PropertyGroup;
import org.linuxforhealth.fhir.core.FHIRMediaType;
@@ -130,6 +128,7 @@ public class BundleTest extends FHIRServerTestBase {
private static final String PATIENT_EXTENSION_URL = "http://my.url.domain.com/acme-healthcare/related-patient";

private static final String PREFER_HEADER_RETURN_REPRESENTATION = "return=representation";
private static final String PREFER_HEADER_RETURN_OPERATION_OUTCOME = "return=OperationOutcome";
private static final String PREFER_HEADER_NAME = "Prefer";

private static Boolean kafkaAuditEnabled = false;
@@ -987,64 +986,69 @@ public void testBatchCompartmentSearch() throws Exception {
@Test(groups = { "batch" }, dependsOnMethods = { "testBatchUpdates" })
public void testBatchMixture() throws Exception {
String method = "testBatchMixture";
WebTarget target = getWebTarget();

// change at least one field so that the update below isn't skipped
patientB1 = patientB1.toBuilder()
.deceased(true)
.build();

// Perform a mixture of request types.
Bundle bundle = buildBundle(BundleType.BATCH);
// create
bundle = addRequestToBundle(null, bundle, HTTPVerb.POST, "Patient", null,
TestUtil.readLocalResource("Patient_DavidOrtiz.json"));
// update
bundle = addRequestToBundle(null, bundle, HTTPVerb.PUT, "Patient/" + patientB1.getId(), null,
patientB1);
// read
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient/" + patientB2.getId(), null, null);
// vread
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, locationB1, null, null);
// history
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient/" + patientB1.getId() + "/_history",
null, null);
// search
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient?family=Ortiz&_count=100", null, null);

printBundle(method, "request", bundle);

Entity<Bundle> entity = Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON);
Response response = target.request()
.header(PREFER_HEADER_NAME, PREFER_HEADER_RETURN_REPRESENTATION)
.post(entity, Response.class);
assertResponse(response, Response.Status.OK.getStatusCode());

Bundle responseBundle = getEntityWithExtraWork(response,method);

assertResponseBundle(responseBundle, BundleType.BATCH_RESPONSE, 6);
assertGoodPostPutResponse(responseBundle.getEntry().get(0), Status.CREATED.getStatusCode());
assertGoodPostPutResponse(responseBundle.getEntry().get(1), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(2), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(3), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(4), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(5), Status.OK.getStatusCode());

Bundle resultSet;

// Verify the history results.
resultSet = (Bundle) responseBundle.getEntry().get(4).getResource();
assertNotNull(resultSet);
assertTrue(resultSet.getEntry().size() > 2);

// Verify the search results.
resultSet = (Bundle) responseBundle.getEntry().get(5).getResource();
assertNotNull(resultSet);
assertTrue(resultSet.getEntry().size() >= 1);

List<String> preferredHeaders = List.of(PREFER_HEADER_RETURN_REPRESENTATION, PREFER_HEADER_RETURN_OPERATION_OUTCOME);
for (String preferredHeader : preferredHeaders) {
WebTarget target = getWebTarget();

patientB1 = (Patient) responseBundle.getEntry().get(1).getResource();
// change at least one field so that the update below isn't skipped
patientB1 = patientB1.toBuilder()
.deceased(true)
.build();

// Perform a mixture of request types.
Bundle bundle = buildBundle(BundleType.BATCH);
// create
bundle = addRequestToBundle(null, bundle, HTTPVerb.POST, "Patient", null,
TestUtil.readLocalResource("Patient_DavidOrtiz.json"));
// update
bundle = addRequestToBundle(null, bundle, HTTPVerb.PUT, "Patient/" + patientB1.getId(), null,
patientB1);
// read
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient/" + patientB2.getId(), null, null);
// vread
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, locationB1, null, null);
// history
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient/" + patientB1.getId() + "/_history",
null, null);
// search
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient?family=Ortiz&_count=100", null, null);

printBundle(method, "request", bundle);

Entity<Bundle> entity = Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON);
Response response = target.request()
.header(PREFER_HEADER_NAME, preferredHeader)
.post(entity, Response.class);
assertResponse(response, Response.Status.OK.getStatusCode());

Bundle responseBundle = getEntityWithExtraWork(response,method);

assertResponseBundle(responseBundle, BundleType.BATCH_RESPONSE, 6);
assertGoodPostPutResponse(responseBundle.getEntry().get(0), Status.CREATED.getStatusCode());
assertGoodPostPutResponse(responseBundle.getEntry().get(1), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(2), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(3), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(4), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(5), Status.OK.getStatusCode());

Bundle resultSet;

// Verify the history results.
resultSet = (Bundle) responseBundle.getEntry().get(4).getResource();
assertNotNull(resultSet);
assertTrue(resultSet.getEntry().size() > 2);

// Verify the search results.
resultSet = (Bundle) responseBundle.getEntry().get(5).getResource();
assertNotNull(resultSet);
assertTrue(resultSet.getEntry().size() >= 1);
if (preferredHeader.equals(PREFER_HEADER_RETURN_REPRESENTATION)) {
patientB1 = (Patient) responseBundle.getEntry().get(1).getResource();
}
}
}


@Test(groups = { "transaction" })
public void testTransactionCreates() throws Exception {
@@ -1542,51 +1546,58 @@ public void testTransactionMixture() throws Exception {
if (!transactionSupported.booleanValue()) {
return;
}

WebTarget target = getWebTarget();

// Perform a mixture of request types.
Bundle bundle = buildBundle(BundleType.TRANSACTION);
// create
bundle = addRequestToBundle(null, bundle, HTTPVerb.POST, "Patient", null,
TestUtil.readLocalResource("Patient_DavidOrtiz.json"));
// update
bundle = addRequestToBundle(null, bundle, HTTPVerb.PUT, "Patient/" + patientT1.getId(), null,
patientT1);
// read
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient/" + patientT2.getId(), null, null);
// vread
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, locationT1, null, null);
// history
// bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient/" + patientT1.getId() + "/_history", null, null);
// search
// bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient?family=Ortiz&_count=100", null, null);

printBundle(method, "request", bundle);

Entity<Bundle> entity = Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON);
Response response = target.request().post(entity, Response.class);
assertResponse(response, Response.Status.OK.getStatusCode());
Bundle responseBundle = getEntityWithExtraWork(response,method);
assertResponseBundle(responseBundle, BundleType.TRANSACTION_RESPONSE, 4);
assertGoodPostPutResponse(responseBundle.getEntry().get(0), Status.CREATED.getStatusCode());
assertGoodPostPutResponse(responseBundle.getEntry().get(1), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(2), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(3), Status.OK.getStatusCode());
// assertGoodGetResponse(responseBundle.getEntry().get(4), Status.OK.getStatusCode());
// assertGoodGetResponse(responseBundle.getEntry().get(5), Status.OK.getStatusCode());

// Bundle resultSet;

// Verify the history results.
// resultSet = (Bundle) FHIRUtil.getResourceContainerResource(responseBundle.getEntry().get(4).getResource());
// assertNotNull(resultSet);
// assertTrue(resultSet.getEntry().size() > 2);

// Verify the search results.
// resultSet = (Bundle) FHIRUtil.getResourceContainerResource(responseBundle.getEntry().get(5).getResource());
// assertNotNull(resultSet);
// assertTrue(resultSet.getEntry().size() > 3);

List<String> preferredHeaders = List.of(PREFER_HEADER_RETURN_REPRESENTATION, PREFER_HEADER_RETURN_OPERATION_OUTCOME);
for (String preferredHeader : preferredHeaders) {
WebTarget target = getWebTarget();

// Perform a mixture of request types.
Bundle bundle = buildBundle(BundleType.TRANSACTION);
// create
bundle = addRequestToBundle(null, bundle, HTTPVerb.POST, "Patient", null,
TestUtil.readLocalResource("Patient_DavidOrtiz.json"));
// update
bundle = addRequestToBundle(null, bundle, HTTPVerb.PUT, "Patient/" + patientT1.getId(), null,
patientT1);
// read
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient/" + patientT2.getId(), null, null);
// vread
bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, locationT1, null, null);
// history
// bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient/" + patientT1.getId() + "/_history", null, null);
// search
// bundle = addRequestToBundle(null, bundle, HTTPVerb.GET, "Patient?family=Ortiz&_count=100", null, null);
Comment on lines +1566 to +1569
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the original test had these commented out too, but it makes me wonder why...


printBundle(method, "request", bundle);

Entity<Bundle> entity = Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON);
Response response = target.request()
.header(PREFER_HEADER_NAME, preferredHeader)
.post(entity, Response.class);
assertResponse(response, Response.Status.OK.getStatusCode());
Bundle responseBundle = getEntityWithExtraWork(response,method);
assertResponseBundle(responseBundle, BundleType.TRANSACTION_RESPONSE, 4);
assertGoodPostPutResponse(responseBundle.getEntry().get(0), Status.CREATED.getStatusCode());
assertGoodPostPutResponse(responseBundle.getEntry().get(1), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(2), Status.OK.getStatusCode());
assertGoodGetResponse(responseBundle.getEntry().get(3), Status.OK.getStatusCode());
// assertGoodGetResponse(responseBundle.getEntry().get(4), Status.OK.getStatusCode());
// assertGoodGetResponse(responseBundle.getEntry().get(5), Status.OK.getStatusCode());

// Bundle resultSet;

// Verify the history results.
// resultSet = (Bundle) FHIRUtil.getResourceContainerResource(responseBundle.getEntry().get(4).getResource());
// assertNotNull(resultSet);
// assertTrue(resultSet.getEntry().size() > 2);

// Verify the search results.
// resultSet = (Bundle) FHIRUtil.getResourceContainerResource(responseBundle.getEntry().get(5).getResource());
// assertNotNull(resultSet);
// assertTrue(resultSet.getEntry().size() > 3);
}


}

@Test(groups = { "batch" })
Original file line number Diff line number Diff line change
@@ -75,7 +75,7 @@ public Response delete(@PathParam("type") String type, @PathParam("id") String i
try {
RestAuditLogger.logDelete(httpServletRequest,
ior != null ? ior.getResource() : null,
startTime, new Date(), status);
startTime, new Date(), status, ior != null ? ior.getFhirResourceContexts() : null);
} catch (Exception e) {
log.log(Level.SEVERE, AUDIT_LOGGING_ERR_MSG, e);
}
@@ -120,7 +120,7 @@ public Response conditionalDelete(@PathParam("type") String type) throws Excepti
try {
RestAuditLogger.logDelete(httpServletRequest,
ior != null ? ior.getResource() : null,
startTime, new Date(), status);
startTime, new Date(), status, ior != null ? ior.getFhirResourceContexts() : null);
} catch (Exception e) {
log.log(Level.SEVERE, AUDIT_LOGGING_ERR_MSG, e);
}
Original file line number Diff line number Diff line change
@@ -8,20 +8,23 @@

import static org.linuxforhealth.fhir.model.type.String.string;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response.Status;

import org.linuxforhealth.fhir.core.FHIRConstants;
import org.linuxforhealth.fhir.exception.FHIROperationException;
import org.linuxforhealth.fhir.model.patch.FHIRPatch;
import org.linuxforhealth.fhir.model.resource.Bundle;
import org.linuxforhealth.fhir.model.resource.Bundle.Entry;
import org.linuxforhealth.fhir.model.resource.OperationOutcome;
import org.linuxforhealth.fhir.model.resource.OperationOutcome.Issue;
import org.linuxforhealth.fhir.model.resource.Resource;
import org.linuxforhealth.fhir.model.type.Extension;
import org.linuxforhealth.fhir.model.util.FHIRUtil;
import org.linuxforhealth.fhir.persistence.SingleResourceResult;
import org.linuxforhealth.fhir.persistence.context.FHIRPersistenceEvent;
@@ -31,6 +34,7 @@
import org.linuxforhealth.fhir.server.exception.FHIRResourceDeletedException;
import org.linuxforhealth.fhir.server.exception.FHIRResourceNotFoundException;
import org.linuxforhealth.fhir.server.spi.operation.FHIROperationContext;
import org.linuxforhealth.fhir.server.spi.operation.FHIRResourceContext;
import org.linuxforhealth.fhir.server.spi.operation.FHIRResourceHelpers;
import org.linuxforhealth.fhir.server.spi.operation.FHIRRestOperationResponse;
import org.linuxforhealth.fhir.server.util.FHIRUrlParser;
@@ -48,6 +52,8 @@ public class FHIRRestInteractionVisitorPersist extends FHIRRestInteractionVisito

// True if the bundle type is TRANSACTION
final boolean transaction;

private static final String EXTENSION_BASE_URL = FHIRConstants.EXT_BASE + "deletedId";

/**
* Public constructor
@@ -234,6 +240,7 @@ public FHIRRestOperationResponse doDelete(int entryIndex, String requestDescript
.status(string(Integer.toString(httpStatus)))
.outcome(oo)
.build())
.extension(buildFHIRResourceContext(ior.getFhirResourceContexts()))
.build();
});

@@ -340,4 +347,29 @@ private void doInteraction(int entryIndex, String requestDescription, long accum
setEntryComplete(entryIndex, entry, requestDescription, accumulatedTime + elapsed);
}
}

/**
* Method to build a list of Extension objects which contains the deleted resources id, type and version Id.
* @param resourceContexts the resourceContext with deleted resource id, type and versionId.
* @return The list of Extension objects with the value in the format {resourceType}/{resourceId}/{versionId}.
*/
private List<Extension> buildFHIRResourceContext(List<FHIRResourceContext> resourceContexts) {
List<Extension> extensions = new ArrayList<>();
if (resourceContexts != null && !resourceContexts.isEmpty()) {
for (FHIRResourceContext resourceContext : resourceContexts) {
if (resourceContext.getId() != null && resourceContext.getResourceType() != null) {
Extension.Builder builder = Extension.builder();
builder.url(EXTENSION_BASE_URL);
StringBuilder extensionValue = new StringBuilder(resourceContext.getResourceType())
.append("/")
.append(resourceContext.getId())
.append("/")
.append(resourceContext.getVersionId());
builder.value(extensionValue.toString());
extensions.add(builder.build());
}
}
}
return extensions;
}
}
Original file line number Diff line number Diff line change
@@ -140,6 +140,7 @@
import org.linuxforhealth.fhir.server.spi.operation.FHIROperation;
import org.linuxforhealth.fhir.server.spi.operation.FHIROperationContext;
import org.linuxforhealth.fhir.server.spi.operation.FHIROperationUtil;
import org.linuxforhealth.fhir.server.spi.operation.FHIRResourceContext;
import org.linuxforhealth.fhir.server.spi.operation.FHIRResourceHelpers;
import org.linuxforhealth.fhir.server.spi.operation.FHIRRestOperationResponse;
import org.linuxforhealth.fhir.validation.FHIRValidator;
@@ -1027,11 +1028,12 @@ public FHIRRestOperationResponse doDelete(String type, String id, String searchQ
}

if (responseBundle != null) {

// adding a list of FHIRResourceContext which will contain the deleted resource id, type and versionId.
List<FHIRResourceContext> resourceContexts = new ArrayList<>();
for (Entry entry: responseBundle.getEntry()) {
id = entry.getResource().getId();
Resource resourceToDelete = entry.getResource();

Copy link
Member

@lmsurpre lmsurpre Jan 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very minor (whitespace removal)

Suggested change

// For soft-delete we store a new version of the resource with the deleted
// flag set. Because we've read the resource already, we need to check that
// the version we are deleting matches the version we just read, so the
@@ -1070,9 +1072,14 @@ public FHIRRestOperationResponse doDelete(String type, String id, String searchQ
final int newVersionNumber = currentVersionNumber + 1;
Resource deletionMarker = FHIRPersistenceUtil.copyAndSetResourceMetaFields(resourceToDelete, resourceToDelete.getId(), newVersionNumber, lastUpdated);
event.setFhirResource(deletionMarker);

// Populate the FHIRResourceContext with resource type, id and versionId. This will be used to populate the audit event for each deleted resource.
resourceContexts.add(new FHIRResourceContext(type, resourceToDelete.getId(), String.valueOf(newVersionNumber)));

getInterceptorMgr().fireAfterDeleteEvent(event);
}

//set the resourceContexts to the FHIRRestOperationResponse
ior.setFhirResourceContexts(resourceContexts);
warnings.add(Issue.builder()
.severity(IssueSeverity.INFORMATION)
.code(IssueType.INFORMATIONAL)
Original file line number Diff line number Diff line change
@@ -8,9 +8,11 @@

import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -33,6 +35,7 @@
import org.linuxforhealth.fhir.config.FHIRConfiguration;
import org.linuxforhealth.fhir.config.FHIRRequestContext;
import org.linuxforhealth.fhir.core.FHIRUtilities;
import org.linuxforhealth.fhir.core.HTTPReturnPreference;
import org.linuxforhealth.fhir.core.util.handler.IPHandler;
import org.linuxforhealth.fhir.model.resource.Basic;
import org.linuxforhealth.fhir.model.resource.Bundle;
@@ -41,11 +44,15 @@
import org.linuxforhealth.fhir.model.type.Code;
import org.linuxforhealth.fhir.model.type.CodeableConcept;
import org.linuxforhealth.fhir.model.type.Coding;
import org.linuxforhealth.fhir.model.type.Extension;
import org.linuxforhealth.fhir.model.type.Id;
import org.linuxforhealth.fhir.model.type.Meta;
import org.linuxforhealth.fhir.model.type.Uri;
import org.linuxforhealth.fhir.model.type.code.BundleType;
import org.linuxforhealth.fhir.model.type.code.HTTPVerb;
import org.linuxforhealth.fhir.model.util.FHIRUtil;
import org.linuxforhealth.fhir.server.spi.operation.FHIRResourceContext;
import static org.linuxforhealth.fhir.model.util.ModelSupport.FHIR_STRING;

/**
* This class provides convenience methods for FHIR Rest services that need to write FHIR audit log entries.
@@ -210,21 +217,33 @@ public static void logRead(HttpServletRequest request, Resource resource, Date s
* The end time of the read request execution.
* @param responseStatus
* The response status.
* @param resourceContexts
* The resource context information.
* @throws Exception
*/
public static void logDelete(HttpServletRequest request, Resource resource, Date startTime, Date endTime, Response.Status responseStatus) throws Exception {
public static void logDelete(HttpServletRequest request, Resource resource, Date startTime, Date endTime, Response.Status responseStatus, List<FHIRResourceContext> resourceContexts) throws Exception {
final String METHODNAME = "logDelete";
log.entering(CLASSNAME, METHODNAME);

AuditLogService auditLogSvc = AuditLogServiceFactory.getService();
if (auditLogSvc.isEnabled()) {
AuditLogEntry entry = initLogEntry(AuditLogEventType.FHIR_DELETE);
populateAuditLogEntry(entry, request, resource, startTime, endTime, responseStatus);

entry.getContext().setAction("D");
entry.setDescription("FHIR Delete request");

auditLogSvc.logEntry(entry);
// populate and log an audit entry with the resource context information for each resource that was successfully deleted.
if (responseStatus == Status.OK && resourceContexts != null && !resourceContexts.isEmpty()) {
for (FHIRResourceContext resourceContext : resourceContexts) {
AuditLogEntry entry = initLogEntry(AuditLogEventType.FHIR_DELETE);
populateAuditLogEntry(entry, request, resource, startTime, endTime, responseStatus);
populateResourceContext(entry, resourceContext);
entry.getContext().setAction("D");
entry.setDescription("FHIR Delete request");
auditLogSvc.logEntry(entry);
}
} else {
// in case of failure log the
AuditLogEntry entry = initLogEntry(AuditLogEventType.FHIR_DELETE);
populateAuditLogEntry(entry, request, resource, startTime, endTime, responseStatus);
entry.getContext().setAction("D");
entry.setDescription("FHIR Delete request");
auditLogSvc.logEntry(entry);
}
}
log.exiting(CLASSNAME, METHODNAME);
}
@@ -392,14 +411,11 @@ private static void logBundleBatch(AuditLogService auditLogSvc, HttpServletReque
Iterator<Bundle.Entry> iter = requestBundle.getEntry().iterator();
for (Entry responseEntry : responseBundle.getEntry()) {
Bundle.Entry requestEntry = iter.next();

AuditLogEntry entry = initLogEntry(AuditLogEventType.FHIR_BUNDLE);

populateAuditLogEntry(entry, request, responseEntry.getResource(), startTime, endTime, responseStatus);
String action = "E";
if (requestEntry.getRequest() != null && requestEntry.getRequest().getMethod() != null) {
boolean operation = requestEntry.getRequest().getUrl().getValue().contains("$")
|| requestEntry.getRequest().getUrl().getValue().contains("/%24");
String action = "E";

HTTPVerb requestMethod = requestEntry.getRequest().getMethod();
switch (HTTPVerb.Value.from(requestMethod.getValue())) {
case GET:
@@ -423,29 +439,35 @@ private static void logBundleBatch(AuditLogService auditLogSvc, HttpServletReque
default:
break;
}

}
String loc = requestEntry.getRequest() != null && requestEntry.getRequest().getUrl() != null ? requestEntry.getRequest().getUrl().getValue() : "";
// if action is delete log an audit entry for each deleted resource
if (action.equals("D") && !(responseEntry.getExtension() == null || responseEntry.getExtension().isEmpty())) {
logBundleDelete(auditLogSvc, responseEntry, request, null, startTime, endTime, responseStatus, loc, "FHIR Bundle Batch request");
} else {
AuditLogEntry entry = initLogEntry(AuditLogEventType.FHIR_BUNDLE);
populateBundleAuditLogEntry(entry, responseEntry, request, responseEntry.getResource(), startTime, endTime, responseStatus);

// Only for BATCH we want to override the REQUEST URI and Status Code
StringBuilder builder = new StringBuilder();
builder.append(request.getRequestURI())
.append(loc);
entry.getContext()
.setApiParameters(
ApiParameters.builder()
.request(builder.toString())
.status(Integer.parseInt(responseEntry.getResponse().getStatus().getValue()))
.build());
entry.getContext().setAction(action);
entry.setDescription("FHIR Bundle Batch request");

// @implNote The audit messages can be batched and sent off to the logEntry.
// The signature would be updated to AuditEntry... entries
// Then a loop and bulk action on Kafka.
auditLogSvc.logEntry(entry);
}

String loc = requestEntry.getRequest().getUrl().getValue();

// Only for BATCH we want to override the REQUEST URI and Status Code
StringBuilder builder = new StringBuilder();
builder.append(request.getRequestURI())
.append("/")
.append(loc);
entry.getContext()
.setApiParameters(
ApiParameters.builder()
.request(builder.toString())
.status(Integer.parseInt(responseEntry.getResponse().getStatus().getValue()))
.build());

entry.setDescription("FHIR Bundle Batch request");

// @implNote The audit messages can be batched and sent off to the logEntry.
// The signature would be updated to AuditEntry... entries
// Then a loop and bulk action on Kafka.
auditLogSvc.logEntry(entry);
}
}
}
@@ -478,11 +500,17 @@ private static void logBundleTransaction(AuditLogService auditLogSvc, HttpServle
Iterator<Bundle.Entry> iter = requestBundle.getEntry().iterator();
for (Entry bundleEntry : responseBundle.getEntry()) {
Bundle.Entry requestEntry = iter.next();
entry = initLogEntry(AuditLogEventType.FHIR_BUNDLE);
populateAuditLogEntry(entry, request, bundleEntry.getResource(), startTime, endTime, responseStatus);
entry.setDescription("FHIR Bundle Transaction request");
entry.getContext().setAction(selectActionForBundleEntry(requestEntry));
auditLogSvc.logEntry(entry);
String action = selectActionForBundleEntry(requestEntry);
// if action is delete log an audit entry for each deleted resource
if (action.equals("D") && !(bundleEntry.getExtension() == null || bundleEntry.getExtension().isEmpty())) {
logBundleDelete(auditLogSvc, bundleEntry, request, null, startTime, endTime, responseStatus, null, "FHIR Bundle Transaction request");
} else {
entry = initLogEntry(AuditLogEventType.FHIR_BUNDLE);
populateBundleAuditLogEntry(entry, bundleEntry, request, bundleEntry.getResource(), startTime, endTime, responseStatus);
entry.setDescription("FHIR Bundle Transaction request");
entry.getContext().setAction(action);
auditLogSvc.logEntry(entry);
}
}
} else {
// log a single audit event message when the batch transaction has failed.
@@ -746,6 +774,142 @@ protected static AuditLogEntry populateAuditLogEntry(AuditLogEntry entry, HttpSe
return entry;
}

/**
* Populate the resource context(resource id, type and version id) into the AuditLogEntry.
* @param entry the audit log entry to be populated.
* @param resourceContext the resource context which needs to be populated into the AuditLogEntry.
* @return AuditLogEntry - an audit log entry with the required resource context populated.
*/
protected static AuditLogEntry populateResourceContext(AuditLogEntry entry, FHIRResourceContext resourceContext) {
final String METHODNAME = "populateResourceContext";
log.entering(CLASSNAME, METHODNAME);
if (resourceContext != null) {
entry.getContext().setData(Data.builder().build());
if (resourceContext.getResourceType() != null) {
entry.getContext().getData().setResourceType(resourceContext.getResourceType());
}
if (resourceContext.getId() != null) {
entry.getContext().getData().setId(resourceContext.getId());
}
if (resourceContext.getVersionId() != null) {
entry.getContext().getData().setVersionId(resourceContext.getVersionId());
}
}
return entry;
}


/**
* Populates the passed audit log entry for Bundle entry.
* This method will populate the resource id, resource type and version id from the
* Bundle response entry location when the return preference is "OperationOutcome".
* When the return preference is "representation" the populateAuditLogEntry method will
* populate the resource id, resource type and version id.
* @param entry
* The AuditLogEntry to be populated.
* @param responseEntry
* The Bundle response entry.
* @param request
* The HttpServletRequest representation of the REST request.
* @param resource
* The Resource object.
* @param startTime
* The start time of the request execution.
* @param endTime
* The end time of the request execution.
* @param responseStatus
* The response status.
* @return AuditLogEntry - an initialized audit log entry.
*/
protected static AuditLogEntry populateBundleAuditLogEntry(AuditLogEntry entry, Bundle.Entry responseEntry, HttpServletRequest request, Resource resource,
Date startTime, Date endTime, Response.Status responseStatus) {
final String METHODNAME = "populateBundleAuditLogEntry";
log.entering(CLASSNAME, METHODNAME);

// call populateAuditLogEntry to populate common attributes to all rest
populateAuditLogEntry(entry, request, resource, startTime, endTime, responseStatus);
if (HTTPReturnPreference.REPRESENTATION.equals(FHIRRequestContext.get().getReturnPreference())) {
return entry;
}
if (responseEntry.getResponse() == null || responseEntry.getResponse().getLocation() == null) {
return entry;
}
// Populate the resource id, resource type and version id from the
// Bundle response entry location when the return preference is "OperationOutcome".
String location = responseEntry.getResponse().getLocation().getValue();
String[] parts = location.split("/");
if (parts.length > 3) {
Collections.reverse(Arrays.asList(parts));
entry.getContext().setData(Data.builder().build());
entry.getContext().getData().setResourceType(parts[3]);
entry.getContext().getData().setVersionId(parts[0]);
entry.getContext().getData().setId(parts[2]);
Comment on lines +831 to +846
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to always populate the AuditLogEntry in the same way (e.g. from the response location)?
do we not set that unless the return preference is OperationOutcome?

}
log.exiting(CLASSNAME, METHODNAME);
return entry;
}

/**
* Log an audit entry for each deleted resource in a Bundle.Entry
* @param auditLogSvc
* The internal FHIR Server API for audit logging.
* @param responseEntry
* The Bundle response entry.
* @param request
* The HttpServletRequest representation of the REST request.
* @param resource
* The Resource object.
* @param startTime
* The start time of the request execution.
* @param endTime
* The end time of the request execution.
* @param responseStatus
* The response status.
* @param location
* @throws Exception
*/
protected static void logBundleDelete(AuditLogService auditLogSvc, Bundle.Entry responseEntry, HttpServletRequest request, Resource resource,
Date startTime, Date endTime, Response.Status responseStatus, String location, String description) throws Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor

Suggested change
Date startTime, Date endTime, Response.Status responseStatus, String location, String description) throws Exception {
Date startTime, Date endTime, Response.Status responseStatus, String location, String description) throws Exception {

final String METHODNAME = "logBundleDelete";
log.entering(CLASSNAME, METHODNAME);

// Populate the resource id, resource type and version id from the
// Bundle response entry extensions
for (Extension extension : responseEntry.getExtension()) {
AuditLogEntry entry = initLogEntry(AuditLogEventType.FHIR_BUNDLE);
populateAuditLogEntry(entry, request, resource, startTime, endTime, responseStatus);
String resourceInfo = extension.getValue().as(FHIR_STRING).getValue();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should double-check the URL of the extension instead of just assuming all extensions will be the ones we're expecting

String[] parts = resourceInfo.split("/");
if (parts.length > 2) {
entry.getContext().setData(Data.builder().build());
entry.getContext().getData().setResourceType(parts[0]);
entry.getContext().getData().setId(parts[1]);
entry.getContext().getData().setVersionId(parts[2]);

}
if (location != null) {
// Only for BATCH we want to override the REQUEST URI and Status Code
StringBuilder builder = new StringBuilder();
builder.append(request.getRequestURI())
.append(location);
entry.getContext()
.setApiParameters(
ApiParameters.builder()
.request(builder.toString())
.status(Integer.parseInt(responseEntry.getResponse().getStatus().getValue()))
.build());
}
entry.getContext().setAction("D");
entry.setDescription(description);

// @implNote The audit messages can be batched and sent off to the logEntry.
// The signature would be updated to AuditEntry... entries
// Then a loop and bulk action on Kafka.
auditLogSvc.logEntry(entry);
}
log.exiting(CLASSNAME, METHODNAME);
}

/**
* Builds and returns an AuditLogEntry with the minimum required fields populated.
*