Skip to content

Commit e4e6240

Browse files
MahiFentayeaschey-forpeople
authored andcommitted
BFD-3666: removing SAMHSA sensitive information based on security tags in the response (#2530)
Co-authored-by: aschey-forpeople <[email protected]>
1 parent 181f055 commit e4e6240

34 files changed

+442
-97
lines changed

apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/SpringConfiguration.java

+3
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ public class SpringConfiguration extends BaseConfiguration {
118118
/** The {@link String } Boolean property that is used to enable the C4DIC profile. */
119119
public static final String SSM_PATH_C4DIC_ENABLED = "c4dic/enabled";
120120

121+
/** The {@link String } Boolean property that is used to enable the samhsa 2.0 profile. */
122+
public static final String SSM_PATH_SAMHSA_V2_ENABLED = "samhsa_v2/enabled";
123+
121124
/** Maximum number of threads to use for executing EOB claim transformers in parallel. */
122125
public static final String PROP_EXECUTOR_SERVICE_THREADS = "bfdServer.executorService.threads";
123126

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package gov.cms.bfd.server.war;
2+
3+
import static gov.cms.bfd.server.war.commons.StringUtils.parseBooleansFromRequest;
4+
5+
import ca.uhn.fhir.rest.api.server.RequestDetails;
6+
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
7+
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOutcome;
8+
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
9+
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
10+
import gov.cms.bfd.server.war.commons.AbstractResourceProvider;
11+
import gov.cms.bfd.server.war.commons.CommonTransformerUtils;
12+
import gov.cms.bfd.sharedutils.TagCode;
13+
import org.hl7.fhir.dstu3.model.Bundle;
14+
import org.hl7.fhir.dstu3.model.Coding;
15+
import org.hl7.fhir.dstu3.model.ExplanationOfBenefit;
16+
import org.hl7.fhir.instance.model.api.IBaseResource;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
/** V1SamhsaConsentInterceptor handles filtering for V1 data types (like V1 Claims). */
21+
public class V1SamhsaConsentInterceptor implements IConsentService {
22+
23+
/** The logger. */
24+
private static final Logger logger = LoggerFactory.getLogger(V1SamhsaConsentInterceptor.class);
25+
26+
@Override
27+
public ConsentOutcome startOperation(
28+
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
29+
return ConsentOutcome.PROCEED;
30+
}
31+
32+
@Override
33+
public ConsentOutcome willSeeResource(
34+
RequestDetails theRequestDetails,
35+
IBaseResource theResource,
36+
IConsentContextServices theContextServices) {
37+
38+
logger.debug("V1SamhsaConsentInterceptor - Processing willSeeResource.");
39+
40+
// Determine if SAMHSA filtering is required from request parameters
41+
boolean excludeSamhsaParam =
42+
parseBooleansFromRequest(theRequestDetails, AbstractResourceProvider.EXCLUDE_SAMHSA)
43+
.stream()
44+
.findFirst()
45+
.orElse(false);
46+
boolean shouldFilterSamhsa =
47+
CommonTransformerUtils.shouldFilterSamhsa(
48+
String.valueOf(excludeSamhsaParam), theRequestDetails);
49+
50+
// No filtering needed, proceed
51+
if (!shouldFilterSamhsa) {
52+
return ConsentOutcome.PROCEED;
53+
}
54+
55+
// Handle Bundle resource type
56+
if (theResource instanceof Bundle bundle) {
57+
for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
58+
if (shouldRedactResource(entry.getResource())) {
59+
redactSensitiveData(entry);
60+
}
61+
}
62+
}
63+
64+
return ConsentOutcome.PROCEED;
65+
}
66+
67+
/**
68+
* Helper method to determine if a resource should be redacted based on security tags.
69+
*
70+
* @param baseResource The resource to check.
71+
* @return true if the resource should be redacted, false otherwise.
72+
*/
73+
private boolean shouldRedactResource(IBaseResource baseResource) {
74+
if (baseResource instanceof ExplanationOfBenefit eob && eob.getMeta() != null) {
75+
76+
for (Coding securityTag : eob.getMeta().getSecurity()) {
77+
// Check for SAMHSA-related tags
78+
if (isSamhsaSecurityTag(securityTag)) {
79+
logger.info("Matched SAMHSA security tag, redacting resource.");
80+
return true;
81+
}
82+
}
83+
}
84+
return false; // No matching SAMHSA tags found
85+
}
86+
87+
/**
88+
* Checks if the security tag is related to SAMHSA (42CFRPart2).
89+
*
90+
* @param securityTag the security Tag
91+
* @return true if it has Samhsa security tag, false otherwise
92+
*/
93+
private boolean isSamhsaSecurityTag(Coding securityTag) {
94+
String code = securityTag.getCode();
95+
return TagCode._42CFRPart2.toString().equalsIgnoreCase(code);
96+
}
97+
98+
/**
99+
* Redacts sensitive data in the resource.
100+
*
101+
* @param entry the entry
102+
*/
103+
private void redactSensitiveData(Bundle.BundleEntryComponent entry) {
104+
logger.debug("Redacting sensitive data from resource.");
105+
entry.setResource(null); // Redact the resource
106+
}
107+
108+
@Override
109+
public void completeOperationSuccess(
110+
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
111+
logger.debug("V1SamhsaConsentInterceptor - completeOperationSuccess.");
112+
}
113+
114+
@Override
115+
public void completeOperationFailure(
116+
RequestDetails theRequestDetails,
117+
BaseServerResponseException theException,
118+
IConsentContextServices theContextServices) {
119+
logger.info("V1SamhsaConsentInterceptor - Operation failed.", theException);
120+
}
121+
}

apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/V1Server.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import ca.uhn.fhir.rest.server.IResourceProvider;
99
import ca.uhn.fhir.rest.server.RestfulServer;
1010
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
11+
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
12+
import gov.cms.bfd.sharedutils.config.ConfigLoader;
1113
import jakarta.servlet.Servlet;
1214
import jakarta.servlet.ServletException;
1315
import jakarta.servlet.annotation.WebServlet;
@@ -98,6 +100,10 @@ protected void initialize() throws ServletException {
98100
springContext.getBean(SpringConfiguration.BLUEBUTTON_STU3_RESOURCE_PROVIDERS, List.class);
99101
setResourceProviders(resourceProviders);
100102

103+
ConfigLoader configLoader = springContext.getBean(ConfigLoader.class);
104+
boolean samhsaV2Enabled =
105+
configLoader.booleanValue(SpringConfiguration.SSM_PATH_SAMHSA_V2_ENABLED);
106+
101107
/*
102108
* Each "plain" provider has one or more annotated methods that provides
103109
* support for non-resource-type methods, such as transaction, and
@@ -127,7 +133,9 @@ protected void initialize() throws ServletException {
127133
// Registers HAPI interceptors to capture request/response time metrics when BFD handlers are
128134
// executed
129135
registerInterceptor(new TimerInterceptor());
130-
136+
if (samhsaV2Enabled) {
137+
registerInterceptor(new ConsentInterceptor(new V1SamhsaConsentInterceptor()));
138+
}
131139
// OpenAPI
132140
OpenApiInterceptor openApiInterceptor = new OpenApiInterceptor();
133141
registerInterceptor(openApiInterceptor);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package gov.cms.bfd.server.war;
2+
3+
import static gov.cms.bfd.server.war.commons.StringUtils.parseBooleansFromRequest;
4+
5+
import ca.uhn.fhir.rest.api.server.RequestDetails;
6+
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
7+
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOutcome;
8+
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
9+
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
10+
import gov.cms.bfd.server.war.commons.AbstractResourceProvider;
11+
import gov.cms.bfd.server.war.commons.CommonTransformerUtils;
12+
import gov.cms.bfd.sharedutils.TagCode;
13+
import org.hl7.fhir.instance.model.api.IBaseCoding;
14+
import org.hl7.fhir.instance.model.api.IBaseResource;
15+
import org.hl7.fhir.r4.model.Bundle;
16+
import org.hl7.fhir.r4.model.Resource;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
/**
21+
* SAMHSAConsentInterceptor handles filtering based on the SAMHSA (42CFRPart2) tags in the request.
22+
* When the feature flag is enabled, the interceptor scrubs resources tagged with the 42CFRPart2
23+
* security tag. If the feature flag is disabled, no filtering occurs.
24+
*/
25+
public class V2SamhsaConsentInterceptor implements IConsentService {
26+
27+
/** The logger. */
28+
private static final Logger logger = LoggerFactory.getLogger(V2SamhsaConsentInterceptor.class);
29+
30+
/**
31+
* Invoked once at the start of every request.
32+
*
33+
* @param theRequestDetails theRequestDetails
34+
* @param theContextServices theContextServices
35+
*/
36+
@Override
37+
public ConsentOutcome startOperation(
38+
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
39+
return ConsentOutcome.PROCEED;
40+
}
41+
42+
/**
43+
* Can a given resource be returned to the user.
44+
*
45+
* @param theRequestDetails theRequestDetails
46+
* @param theResource theResource
47+
* @param theContextServices theContextServices
48+
*/
49+
@Override
50+
public ConsentOutcome willSeeResource(
51+
RequestDetails theRequestDetails,
52+
IBaseResource theResource,
53+
IConsentContextServices theContextServices) {
54+
55+
logger.debug("SAMHSAConsentInterceptor - Processing willSeeResource.");
56+
57+
// Determine if SAMHSA filtering is required from request parameters
58+
boolean excludeSamhsaParam =
59+
parseBooleansFromRequest(theRequestDetails, AbstractResourceProvider.EXCLUDE_SAMHSA)
60+
.stream()
61+
.findFirst()
62+
.orElse(false);
63+
boolean shouldFilterSamhsa =
64+
CommonTransformerUtils.shouldFilterSamhsa(
65+
String.valueOf(excludeSamhsaParam), theRequestDetails);
66+
67+
if (!shouldFilterSamhsa) {
68+
return ConsentOutcome.PROCEED; // No filtering needed, proceed
69+
}
70+
71+
// If the resource is a Bundle, check each entry for SAMHSA security tags
72+
if (theResource instanceof Bundle bundle) {
73+
for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
74+
if (shouldRedactResource(entry.getResource())) {
75+
redactSensitiveData(entry);
76+
}
77+
}
78+
}
79+
80+
return ConsentOutcome.PROCEED;
81+
}
82+
83+
/**
84+
* Checks if a resource should be redacted based on SAMHSA security tags.
85+
*
86+
* @param baseResource The resource to check.
87+
* @return true if the resource should be redacted, false otherwise.
88+
*/
89+
private boolean shouldRedactResource(IBaseResource baseResource) {
90+
if (baseResource instanceof Resource resource && resource.getMeta() != null) {
91+
for (IBaseCoding securityTag : resource.getMeta().getSecurity()) {
92+
// Check for SAMHSA-related tags
93+
if (isSamhsaSecurityTag(securityTag)) {
94+
logger.info("Matched SAMHSA security tag, redacting resource.");
95+
return true;
96+
}
97+
}
98+
}
99+
return false; // No matching SAMHSA tags found
100+
}
101+
102+
/**
103+
* Determines if a security tag is related to SAMHSA (42CFRPart2).
104+
*
105+
* @param securityTag the security tag
106+
* @return true if it has Samhsa security tag, false otherwise
107+
*/
108+
private boolean isSamhsaSecurityTag(IBaseCoding securityTag) {
109+
String code = securityTag.getCode();
110+
return TagCode._42CFRPart2.toString().equalsIgnoreCase(code);
111+
}
112+
113+
/**
114+
* Redacts sensitive data in the resource.
115+
*
116+
* @param entry the entry
117+
*/
118+
private void redactSensitiveData(Bundle.BundleEntryComponent entry) {
119+
logger.debug("V2SamhsaConsentInterceptor - redactSensitiveData.");
120+
entry.setResource(null);
121+
}
122+
123+
@Override
124+
public void completeOperationSuccess(
125+
RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
126+
logger.debug("V2SamhsaConsentInterceptor - completeOperationSuccess.");
127+
}
128+
129+
@Override
130+
public void completeOperationFailure(
131+
RequestDetails theRequestDetails,
132+
BaseServerResponseException theException,
133+
IConsentContextServices theContextServices) {
134+
logger.info("V2SamhsaConsentInterceptor - Operation failed.", theException);
135+
}
136+
}

apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/V2Server.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import ca.uhn.fhir.rest.server.IResourceProvider;
99
import ca.uhn.fhir.rest.server.RestfulServer;
1010
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
11+
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
1112
import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider;
13+
import gov.cms.bfd.sharedutils.config.ConfigLoader;
1214
import jakarta.servlet.Servlet;
1315
import jakarta.servlet.ServletException;
1416
import jakarta.servlet.annotation.WebServlet;
@@ -106,6 +108,10 @@ protected void initialize() throws ServletException {
106108
springContext.getBean(SpringConfiguration.BLUEBUTTON_R4_RESOURCE_PROVIDERS, List.class);
107109
setResourceProviders(resourceProviders);
108110

111+
ConfigLoader configLoader = springContext.getBean(ConfigLoader.class);
112+
boolean samhsaV2Enabled =
113+
configLoader.booleanValue(SpringConfiguration.SSM_PATH_SAMHSA_V2_ENABLED);
114+
109115
/*
110116
* Each "plain" provider has one or more annotated methods that provides
111117
* support for non-resource-type methods, such as transaction, and
@@ -135,7 +141,9 @@ protected void initialize() throws ServletException {
135141
// Registers HAPI interceptors to capture request/response time metrics when BFD handlers are
136142
// executed
137143
registerInterceptor(new TimerInterceptor());
138-
144+
if (samhsaV2Enabled) {
145+
registerInterceptor(new ConsentInterceptor(new V2SamhsaConsentInterceptor()));
146+
}
139147
// OpenAPI
140148
OpenApiInterceptor openApiInterceptor = new OpenApiInterceptor();
141149
registerInterceptor(openApiInterceptor);

apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/AbstractResourceProvider.java

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public class AbstractResourceProvider {
1717
*/
1818
public static final String HEADER_NAME_INCLUDE_TAX_NUMBERS = "IncludeTaxNumbers";
1919

20+
/** A constant for excludeSAMHSA. */
21+
public static final String EXCLUDE_SAMHSA = "excludeSAMHSA";
22+
2023
/**
2124
* Returns if tax numbers should be included after examining the request details.
2225
*

apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/StringUtils.java

+14
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ public static String[] splitOnCommas(String input) {
3939
return builder.build().toArray(new String[0]);
4040
}
4141

42+
/**
43+
* Custom function to retrieve the boolean value from a string input.
44+
*
45+
* @param requestDetails the request details.
46+
* @param parameterToParse the parameter To Parse.
47+
* @return true or false.
48+
*/
49+
public static List<Boolean> parseBooleansFromRequest(
50+
RequestDetails requestDetails, String parameterToParse) {
51+
return getParametersFromRequest(requestDetails, parameterToParse)
52+
.map(Boolean::parseBoolean)
53+
.toList();
54+
}
55+
4256
/**
4357
* Attempts to parse the input as a Long, throwing a bad request error if it fails.
4458
*

0 commit comments

Comments
 (0)