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

BFD-3666: removing SAMHSA sensitive information based on security tags in the response #2530

Merged
merged 12 commits into from
Feb 6, 2025
Merged
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
Expand Up @@ -118,6 +118,9 @@ public class SpringConfiguration extends BaseConfiguration {
/** The {@link String } Boolean property that is used to enable the C4DIC profile. */
public static final String SSM_PATH_C4DIC_ENABLED = "c4dic/enabled";

/** The {@link String } Boolean property that is used to enable the samhsa 2.0 profile. */
public static final String SSM_PATH_SAMHSA_V2_ENABLED = "samhsa_v2/enabled";

/** Maximum number of threads to use for executing EOB claim transformers in parallel. */
public static final String PROP_EXECUTOR_SERVICE_THREADS = "bfdServer.executorService.threads";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package gov.cms.bfd.server.war;

import static gov.cms.bfd.server.war.commons.StringUtils.parseBooleansFromRequest;

import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOutcome;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
import gov.cms.bfd.server.war.commons.AbstractResourceProvider;
import gov.cms.bfd.server.war.commons.CommonTransformerUtils;
import gov.cms.bfd.sharedutils.TagCode;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.ExplanationOfBenefit;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** V1SamhsaConsentInterceptor handles filtering for V1 data types (like V1 Claims). */
public class V1SamhsaConsentInterceptor implements IConsentService {

/** The logger. */
private static final Logger logger = LoggerFactory.getLogger(V1SamhsaConsentInterceptor.class);

@Override
public ConsentOutcome startOperation(
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}

@Override
public ConsentOutcome willSeeResource(
RequestDetails theRequestDetails,
IBaseResource theResource,
IConsentContextServices theContextServices) {

logger.debug("V1SamhsaConsentInterceptor - Processing willSeeResource.");

// Determine if SAMHSA filtering is required from request parameters
boolean excludeSamhsaParam =
parseBooleansFromRequest(theRequestDetails, AbstractResourceProvider.EXCLUDE_SAMHSA)
.stream()
.findFirst()
.orElse(false);
boolean shouldFilterSamhsa =
CommonTransformerUtils.shouldFilterSamhsa(
String.valueOf(excludeSamhsaParam), theRequestDetails);

// No filtering needed, proceed
if (!shouldFilterSamhsa) {
return ConsentOutcome.PROCEED;
}

// Handle Bundle resource type
if (theResource instanceof Bundle bundle) {
for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
if (shouldRedactResource(entry.getResource())) {
redactSensitiveData(entry);
}
}
}

return ConsentOutcome.PROCEED;
}

/**
* Helper method to determine if a resource should be redacted based on security tags.
*
* @param baseResource The resource to check.
* @return true if the resource should be redacted, false otherwise.
*/
private boolean shouldRedactResource(IBaseResource baseResource) {
if (baseResource instanceof ExplanationOfBenefit eob && eob.getMeta() != null) {

for (Coding securityTag : eob.getMeta().getSecurity()) {
// Check for SAMHSA-related tags
if (isSamhsaSecurityTag(securityTag)) {
logger.info("Matched SAMHSA security tag, redacting resource.");
return true;
}
}
}
return false; // No matching SAMHSA tags found
}

/**
* Checks if the security tag is related to SAMHSA (42CFRPart2).
*
* @param securityTag the security Tag
* @return true if it has Samhsa security tag, false otherwise
*/
private boolean isSamhsaSecurityTag(Coding securityTag) {
String code = securityTag.getCode();
return TagCode._42CFRPart2.toString().equalsIgnoreCase(code);
}

/**
* Redacts sensitive data in the resource.
*
* @param entry the entry
*/
private void redactSensitiveData(Bundle.BundleEntryComponent entry) {
logger.debug("Redacting sensitive data from resource.");
entry.setResource(null); // Redact the resource
}

@Override
public void completeOperationSuccess(
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
logger.debug("V1SamhsaConsentInterceptor - completeOperationSuccess.");
}

@Override
public void completeOperationFailure(
RequestDetails theRequestDetails,
BaseServerResponseException theException,
IConsentContextServices theContextServices) {
logger.info("V1SamhsaConsentInterceptor - Operation failed.", theException);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
import gov.cms.bfd.sharedutils.config.ConfigLoader;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
Expand Down Expand Up @@ -98,6 +100,10 @@ protected void initialize() throws ServletException {
springContext.getBean(SpringConfiguration.BLUEBUTTON_STU3_RESOURCE_PROVIDERS, List.class);
setResourceProviders(resourceProviders);

ConfigLoader configLoader = springContext.getBean(ConfigLoader.class);
boolean samhsaV2Enabled =
configLoader.booleanValue(SpringConfiguration.SSM_PATH_SAMHSA_V2_ENABLED);

/*
* Each "plain" provider has one or more annotated methods that provides
* support for non-resource-type methods, such as transaction, and
Expand Down Expand Up @@ -127,7 +133,9 @@ protected void initialize() throws ServletException {
// Registers HAPI interceptors to capture request/response time metrics when BFD handlers are
// executed
registerInterceptor(new TimerInterceptor());

if (samhsaV2Enabled) {
registerInterceptor(new ConsentInterceptor(new V1SamhsaConsentInterceptor()));
}
// OpenAPI
OpenApiInterceptor openApiInterceptor = new OpenApiInterceptor();
registerInterceptor(openApiInterceptor);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package gov.cms.bfd.server.war;

import static gov.cms.bfd.server.war.commons.StringUtils.parseBooleansFromRequest;

import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOutcome;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
import gov.cms.bfd.server.war.commons.AbstractResourceProvider;
import gov.cms.bfd.server.war.commons.CommonTransformerUtils;
import gov.cms.bfd.sharedutils.TagCode;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* SAMHSAConsentInterceptor handles filtering based on the SAMHSA (42CFRPart2) tags in the request.
* When the feature flag is enabled, the interceptor scrubs resources tagged with the 42CFRPart2
* security tag. If the feature flag is disabled, no filtering occurs.
*/
public class V2SamhsaConsentInterceptor implements IConsentService {

/** The logger. */
private static final Logger logger = LoggerFactory.getLogger(V2SamhsaConsentInterceptor.class);

/**
* Invoked once at the start of every request.
*
* @param theRequestDetails theRequestDetails
* @param theContextServices theContextServices
*/
@Override
public ConsentOutcome startOperation(
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}

/**
* Can a given resource be returned to the user.
*
* @param theRequestDetails theRequestDetails
* @param theResource theResource
* @param theContextServices theContextServices
*/
@Override
public ConsentOutcome willSeeResource(
RequestDetails theRequestDetails,
IBaseResource theResource,
IConsentContextServices theContextServices) {

logger.debug("SAMHSAConsentInterceptor - Processing willSeeResource.");

// Determine if SAMHSA filtering is required from request parameters
boolean excludeSamhsaParam =
parseBooleansFromRequest(theRequestDetails, AbstractResourceProvider.EXCLUDE_SAMHSA)
.stream()
.findFirst()
.orElse(false);
boolean shouldFilterSamhsa =
CommonTransformerUtils.shouldFilterSamhsa(
String.valueOf(excludeSamhsaParam), theRequestDetails);

if (!shouldFilterSamhsa) {
return ConsentOutcome.PROCEED; // No filtering needed, proceed
}

// If the resource is a Bundle, check each entry for SAMHSA security tags
if (theResource instanceof Bundle bundle) {
for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
if (shouldRedactResource(entry.getResource())) {
redactSensitiveData(entry);
}
}
}

return ConsentOutcome.PROCEED;
}

/**
* Checks if a resource should be redacted based on SAMHSA security tags.
*
* @param baseResource The resource to check.
* @return true if the resource should be redacted, false otherwise.
*/
private boolean shouldRedactResource(IBaseResource baseResource) {
if (baseResource instanceof Resource resource && resource.getMeta() != null) {
for (IBaseCoding securityTag : resource.getMeta().getSecurity()) {
// Check for SAMHSA-related tags
if (isSamhsaSecurityTag(securityTag)) {
logger.info("Matched SAMHSA security tag, redacting resource.");
return true;
}
}
}
return false; // No matching SAMHSA tags found
}

/**
* Determines if a security tag is related to SAMHSA (42CFRPart2).
*
* @param securityTag the security tag
* @return true if it has Samhsa security tag, false otherwise
*/
private boolean isSamhsaSecurityTag(IBaseCoding securityTag) {
String code = securityTag.getCode();
return TagCode._42CFRPart2.toString().equalsIgnoreCase(code);
}

/**
* Redacts sensitive data in the resource.
*
* @param entry the entry
*/
private void redactSensitiveData(Bundle.BundleEntryComponent entry) {
logger.debug("V2SamhsaConsentInterceptor - redactSensitiveData.");
entry.setResource(null);
}

@Override
public void completeOperationSuccess(
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
logger.debug("V2SamhsaConsentInterceptor - completeOperationSuccess.");
}

@Override
public void completeOperationFailure(
RequestDetails theRequestDetails,
BaseServerResponseException theException,
IConsentContextServices theContextServices) {
logger.info("V2SamhsaConsentInterceptor - Operation failed.", theException);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider;
import gov.cms.bfd.sharedutils.config.ConfigLoader;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
Expand Down Expand Up @@ -106,6 +108,10 @@ protected void initialize() throws ServletException {
springContext.getBean(SpringConfiguration.BLUEBUTTON_R4_RESOURCE_PROVIDERS, List.class);
setResourceProviders(resourceProviders);

ConfigLoader configLoader = springContext.getBean(ConfigLoader.class);
boolean samhsaV2Enabled =
configLoader.booleanValue(SpringConfiguration.SSM_PATH_SAMHSA_V2_ENABLED);

/*
* Each "plain" provider has one or more annotated methods that provides
* support for non-resource-type methods, such as transaction, and
Expand Down Expand Up @@ -135,7 +141,9 @@ protected void initialize() throws ServletException {
// Registers HAPI interceptors to capture request/response time metrics when BFD handlers are
// executed
registerInterceptor(new TimerInterceptor());

if (samhsaV2Enabled) {
registerInterceptor(new ConsentInterceptor(new V2SamhsaConsentInterceptor()));
}
// OpenAPI
OpenApiInterceptor openApiInterceptor = new OpenApiInterceptor();
registerInterceptor(openApiInterceptor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public class AbstractResourceProvider {
*/
public static final String HEADER_NAME_INCLUDE_TAX_NUMBERS = "IncludeTaxNumbers";

/** A constant for excludeSAMHSA. */
public static final String EXCLUDE_SAMHSA = "excludeSAMHSA";

/**
* Returns if tax numbers should be included after examining the request details.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ public static String[] splitOnCommas(String input) {
return builder.build().toArray(new String[0]);
}

/**
* Custom function to retrieve the boolean value from a string input.
*
* @param requestDetails the request details.
* @param parameterToParse the parameter To Parse.
* @return true or false.
*/
public static List<Boolean> parseBooleansFromRequest(
RequestDetails requestDetails, String parameterToParse) {
return getParametersFromRequest(requestDetails, parameterToParse)
.map(Boolean::parseBoolean)
.toList();
}

/**
* Attempts to parse the input as a Long, throwing a bad request error if it fails.
*
Expand Down
Loading