Skip to content
Draft
Show file tree
Hide file tree
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,85 @@
package com.google.fhir.gateway;

import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.storage.interceptor.balp.IBalpAuditContextServices;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import jakarta.annotation.Nonnull;
import org.apache.http.HttpHeaders;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Reference;
import org.jetbrains.annotations.NotNull;

public class BalpAuditContextService implements IBalpAuditContextServices {

private static final String BEARER_PREFIX = "Bearer ";
private static final String CLAIM_NAME = "name";
private static final String CLAIM_PREFERRED_NAME = "preferred_username";
private static final String CLAIM_SUBJECT = "sub";

@Override
public @NotNull Reference getAgentClientWho(RequestDetails requestDetails) {

return new Reference()
// .setReference("Device/fhir-info-gateway")
.setType("Device")
.setDisplay("FHIR Info Gateway")
.setIdentifier(
new Identifier()
.setSystem("http://fhir-info-gateway/devices")
.setValue("fhir-info-gateway-001"));
}

@Override
public @NotNull Reference getAgentUserWho(RequestDetails requestDetails) {

String username = getClaimIfExists(requestDetails, CLAIM_PREFERRED_NAME);
String name = getClaimIfExists(requestDetails, CLAIM_NAME);
String subject = getClaimIfExists(requestDetails, CLAIM_SUBJECT);

return new Reference()
// .setReference("Practitioner/" + subject)
.setType("Practitioner")
.setDisplay(name)
.setIdentifier(
new Identifier()
.setSystem("http://fhir-info-gateway/practitioners")
.setValue(username));
}

@Override
public @NotNull String massageResourceIdForStorage(
@Nonnull RequestDetails theRequestDetails,
@Nonnull IBaseResource theResource,
@Nonnull IIdType theResourceId) {

/**
* Server not configured to allow external references resulting to InvalidRequestException: HTTP
* 400 : HAPI-0507: Resource contains external reference to URL. Here we should use relative
* references instead e.g. Patient/123;
*/
// String serverBaseUrl = theRequestDetails.getFhirServerBase();
return theRequestDetails.getId() != null
? theRequestDetails.getId().getValue()
: ""; // For entity POST there will be no agent.who entry reference since not generated yet
}

public String getClaimIfExists(RequestDetails requestDetails, String claimName) {
String claim;
try {
String authHeader = requestDetails.getHeader(HttpHeaders.AUTHORIZATION);
String bearerToken = authHeader.substring(BEARER_PREFIX.length());
DecodedJWT jwt;

jwt = JWT.decode(bearerToken);
claim = JwtUtil.getClaimOrDie(jwt, claimName);
} catch (JWTDecodeException | AuthenticationException e) {
claim = "";
}
return claim;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.google.fhir.gateway;

import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.storage.interceptor.balp.IBalpAuditEventSink;
import org.hl7.fhir.r4.model.AuditEvent;

public class BalpAuditEventSink implements IBalpAuditEventSink {

private final IGenericClient genericClient;

public BalpAuditEventSink(IGenericClient genericClient) {
this.genericClient = genericClient;
}

@Override
public void recordAuditEvent(AuditEvent auditEvent) {
genericClient.create().resource(auditEvent).prettyPrint().encodedJson().execute();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.IRestfulResponse;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.storage.interceptor.balp.BalpAuditCaptureInterceptor;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
Expand All @@ -47,6 +52,7 @@
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.util.EntityUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.DomainResource;
import org.slf4j.Logger;
Expand Down Expand Up @@ -156,6 +162,13 @@ public boolean authorizeRequest(RequestDetails requestDetails) {
mutateRequest(requestDetails, outcome);
logger.debug("Authorized request path " + requestPath);
try {
// We need to get the resource first before forwarding the delete operation, otherwise we
// can't retrieve it for audit logging
var resourceToDelete =
requestDetails.getRequestType() == RequestTypeEnum.DELETE
? this.fetchStoredResource(requestDetails)
: null;

HttpResponse response = fhirClient.handleRequest(servletDetails);
HttpUtil.validateResponseEntityExistsOrFail(response, requestPath);
// TODO communicate post-processing failures to the client; see:
Expand All @@ -169,27 +182,80 @@ public boolean authorizeRequest(RequestDetails requestDetails) {
if (outcome.getBalpAuditEventSink() != null
&& outcome.getBalpAuditContextService() != null) {

BalpAuditCaptureInterceptor balpInterceptor =
new BalpAuditCaptureInterceptor(
outcome.getBalpAuditEventSink(), outcome.getBalpAuditContextService());

// PoC - for now processing non-Bundle requests only
switch (requestDetails.getRequestType()) {
case GET: // For reads and vReads
servletDetails.setRestOperationType(RestOperationTypeEnum.READ);
// Note, here we are consuming the stream so expect subsequent reads to fail. For
// PoC purposes only
var results = EntityUtils.toString(response.getEntity());
var parsedFHIRresources =
server.getFhirContext().newJsonParser().parseResource(results);

SimplePreResourceShowDetails preResourceShowDetails =
new SimplePreResourceShowDetails(parsedFHIRresources);
HookParams preShowParams =
new HookParams()
.add(RequestDetails.class, requestDetails)
.addIfMatchesType(ServletRequestDetails.class, servletDetails)
.add(IPreResourceShowDetails.class, preResourceShowDetails);
server
.getInterceptorService()
.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, preShowParams);
break;

case POST:
servletDetails.setRestOperationType(RestOperationTypeEnum.CREATE);
balpInterceptor.hookStoragePrecommitResourceCreated(
this.getResource(requestDetails), servletDetails);

TransactionDetails theTransactionDetails = new TransactionDetails();
HookParams hookParams =
new HookParams()
.add(IBaseResource.class, this.getResource(requestDetails))
.add(RequestDetails.class, requestDetails)
.addIfMatchesType(ServletRequestDetails.class, servletDetails)
.add(TransactionDetails.class, theTransactionDetails)
.add(
InterceptorInvocationTimingEnum.class,
theTransactionDetails.getInvocationTiming(
Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
server
.getInterceptorService()
.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams);

break;
case PUT:
servletDetails.setRestOperationType(RestOperationTypeEnum.UPDATE);
IBaseResource oldResource = this.fetchStoredResource(requestDetails);
balpInterceptor.hookStoragePrecommitResourceUpdated(
oldResource, this.getResource(requestDetails), servletDetails);
IBaseResource oldResource = this.fetchStoredResourcePreviousVersion(requestDetails);

HookParams params2 =
new HookParams()
.add(IBaseResource.class, oldResource)
.add(IBaseResource.class, this.getResource(requestDetails))
.add(RequestDetails.class, requestDetails)
.addIfMatchesType(ServletRequestDetails.class, servletDetails);
server
.getInterceptorService()
.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, params2);

break;
case DELETE:
servletDetails.setRestOperationType(RestOperationTypeEnum.DELETE);
balpInterceptor.hookStoragePrecommitResourceDeleted(
this.getResource(requestDetails), servletDetails);

TransactionDetails deleteTransactionDetails = new TransactionDetails();
HookParams preCommitParams =
new HookParams()
.add(RequestDetails.class, requestDetails)
.addIfMatchesType(ServletRequestDetails.class, servletDetails)
.add(IBaseResource.class, resourceToDelete)
.add(TransactionDetails.class, deleteTransactionDetails)
.add(
InterceptorInvocationTimingEnum.class,
deleteTransactionDetails.getInvocationTiming(
Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
server
.getInterceptorService()
.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, preCommitParams);

break;
default:
break;
Expand Down Expand Up @@ -253,19 +319,10 @@ private IBaseResource getResource(RequestDetails requestDetails) {
return null;
}

private IBaseResource fetchStoredResource(RequestDetails requestDetails) {
private IBaseResource fetchStoredResourcePreviousVersion(RequestDetails requestDetails) {
IGenericClient genericClient =
server.getFhirContext().newRestfulGenericClient(fhirClient.getBaseUrl());
IBaseResource current =
genericClient
.read()
.resource(
server
.getFhirContext()
.getResourceDefinition(requestDetails.getResourceName())
.getImplementingClass())
.withId(requestDetails.getId())
.execute();
IBaseResource current = fetchStoredResource(requestDetails);
String currentVersion = current.getMeta().getVersionId();
int prevVersion = Integer.parseInt(currentVersion) - 1;

Expand All @@ -283,6 +340,20 @@ private IBaseResource fetchStoredResource(RequestDetails requestDetails) {
return previous;
}

private IBaseResource fetchStoredResource(RequestDetails requestDetails) {
IGenericClient genericClient =
server.getFhirContext().newRestfulGenericClient(fhirClient.getBaseUrl());
return genericClient
.read()
.resource(
server
.getFhirContext()
.getResourceDefinition(requestDetails.getResourceName())
.getImplementingClass())
.withId(requestDetails.getId())
.execute();
}

private boolean sendGzippedResponse(ServletRequestDetails requestDetails) {
// we send gzipped encoded response to client only if they requested so
String acceptEncodingValue = requestDetails.getHeader(ACCEPT_ENCODING_HEADER.toLowerCase());
Expand Down
16 changes: 14 additions & 2 deletions server/src/main/java/com/google/fhir/gateway/FhirProxyServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.ApacheProxyAddressStrategy;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import ca.uhn.fhir.storage.interceptor.balp.BalpAuditCaptureInterceptor;
import com.google.fhir.gateway.interfaces.AccessCheckerFactory;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
Expand Down Expand Up @@ -62,11 +64,21 @@ protected void initialize() throws ServletException {

// Note interceptor registration order is important.
registerCorsInterceptor();

try {

HttpFhirClient httpFhirClient = FhirClientFactory.createFhirClientFromEnvVars();

IGenericClient genericClient =
getFhirContext().newRestfulGenericClient(httpFhirClient.getBaseUrl());

BalpAuditCaptureInterceptor balpInterceptor =
new BalpAuditCaptureInterceptor(
new BalpAuditEventSink(genericClient), new BalpAuditContextService());

registerInterceptor(balpInterceptor);

logger.info("Adding BearerAuthorizationInterceptor ");
AccessCheckerFactory checkerFactory = chooseAccessCheckerFactory();
HttpFhirClient httpFhirClient = FhirClientFactory.createFhirClientFromEnvVars();
TokenVerifier tokenVerifier = TokenVerifier.createFromEnvVars();
registerInterceptor(
new BearerAuthorizationInterceptor(
Expand Down
Loading