Skip to content

Commit 18d50fd

Browse files
AbayomiE-TAbayomi Adebowale
and
Abayomi Adebowale
authoredMar 20, 2025··
Fixed an issue where bundles containing an operation outcome from a search get blocked from access. (#6800)
* Merged master into branch * Fixed an issue where bundles containing an operation outcome from a search get blocked from access. * addressed comments * ran 'mvn spotless apply' * added a javadoc to AuthorizationInterceptor.toListOfResourcesAndExcludeOperationOutcomeBasedOnRestOperationType() --------- Co-authored-by: Abayomi Adebowale <abayomi.adebowale@smiledigitalhealth.com>
1 parent f339b8c commit 18d50fd

File tree

4 files changed

+78
-9
lines changed

4 files changed

+78
-9
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
type: fix
3+
issue: 6799
4+
title: "Fixed an issue where bundles containing an operation outcome from a search cannot be accessed without explicit
5+
read permissions given for the OperationOutcome resource."

‎hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java

+33-6
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ public class AuthorizationInterceptor implements IRuleApplier {
9191
AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_RULELIST";
9292
private PolicyEnum myDefaultPolicy = PolicyEnum.DENY;
9393
private Set<AuthorizationFlagsEnum> myFlags = Collections.emptySet();
94+
public static final List<RestOperationTypeEnum> REST_OPERATIONS_TO_EXCLUDE_SECURITY_FOR_OPERATION_OUTCOME = List.of(
95+
RestOperationTypeEnum.SEARCH_TYPE, RestOperationTypeEnum.SEARCH_SYSTEM, RestOperationTypeEnum.GET_PAGE);
9496
private IValidationSupport myValidationSupport;
9597

9698
private IAuthorizationSearchParamMatcher myAuthorizationSearchParamMatcher;
@@ -512,7 +514,6 @@ private void checkOutgoingResourceAndFailIfDeny(
512514
if (alreadySeenMap.putIfAbsent(theResponseObject, Boolean.TRUE) != null) {
513515
return;
514516
}
515-
516517
FhirContext fhirContext = theRequestDetails.getServer().getFhirContext();
517518
List<IBaseResource> resources = Collections.emptyList();
518519

@@ -529,7 +530,8 @@ private void checkOutgoingResourceAndFailIfDeny(
529530
case EXTENDED_OPERATION_TYPE:
530531
case EXTENDED_OPERATION_INSTANCE: {
531532
if (theResponseObject != null) {
532-
resources = toListOfResourcesAndExcludeContainerUnlessStandalone(theResponseObject, fhirContext);
533+
resources = toListOfResourcesAndExcludeContainerUnlessStandalone(
534+
theResponseObject, fhirContext, theRequestDetails);
533535
}
534536
break;
535537
}
@@ -577,21 +579,46 @@ private enum OperationExamineDirection {
577579
}
578580

579581
protected static List<IBaseResource> toListOfResourcesAndExcludeContainerUnlessStandalone(
580-
IBaseResource theResponseObject, FhirContext fhirContext) {
582+
IBaseResource theResponseObject, FhirContext fhirContext, RequestDetails theRequestDetails) {
583+
581584
if (theResponseObject == null) {
582585
return Collections.emptyList();
583586
}
584587

585-
List<IBaseResource> retVal;
586-
587588
boolean shouldExamineChildResources = shouldExamineChildResources(theResponseObject, fhirContext);
588589
if (!shouldExamineChildResources) {
589-
return Collections.singletonList(theResponseObject);
590+
return toListOfResourcesAndExcludeOperationOutcomeBasedOnRestOperationType(
591+
theResponseObject, theRequestDetails);
590592
}
591593

592594
return toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext);
593595
}
594596

597+
/**
598+
*
599+
* @param theResponseObject The resource to convert to a list.
600+
* @param theRequestDetails The request details.
601+
* @return The response object (a resource) as a list. If the REST operation type in the request details is a
602+
* search, and the search is for resources that aren't the OperationOutcome, any OperationOutcome resource is removed from the list.
603+
* e.g. A GET [base]/Patient?parameter(s) search may return a bundle containing an OperationOutcome. The OperationOutcome will be removed from the
604+
* list to exclude from security.
605+
*/
606+
private static List<IBaseResource> toListOfResourcesAndExcludeOperationOutcomeBasedOnRestOperationType(
607+
IBaseResource theResponseObject, RequestDetails theRequestDetails) {
608+
List<IBaseResource> resources = new ArrayList<>();
609+
RestOperationTypeEnum restOperationType = theRequestDetails.getRestOperationType();
610+
String resourceName = theRequestDetails.getResourceName();
611+
resources.add(theResponseObject);
612+
613+
if (resourceName != null
614+
&& !resourceName.equals("OperationOutcome")
615+
&& REST_OPERATIONS_TO_EXCLUDE_SECURITY_FOR_OPERATION_OUTCOME.contains(restOperationType)) {
616+
resources.removeIf(t -> t instanceof IBaseOperationOutcome);
617+
}
618+
619+
return resources;
620+
}
621+
595622
@Nonnull
596623
public static List<IBaseResource> toListOfResourcesAndExcludeContainer(
597624
IBaseResource theResponseObject, FhirContext fhirContext) {

‎hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,7 @@ private static Verdict applyRulesToResponseBundle(
842842
Pointcut thePointcut) {
843843
List<IBaseResource> outputResources =
844844
AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(
845-
theOutputResource, theRequestDetails.getFhirContext());
845+
theOutputResource, theRequestDetails.getFhirContext(), theRequestDetails);
846846
return applyRulesToResponseResources(theRequestDetails, theRuleApplier, thePointcut, outputResources);
847847
}
848848

‎hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java

+39-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@
9292
import org.junit.jupiter.api.Test;
9393
import org.junit.jupiter.api.extension.RegisterExtension;
9494
import org.junit.jupiter.params.ParameterizedTest;
95+
import org.junit.jupiter.params.provider.Arguments;
9596
import org.junit.jupiter.params.provider.EnumSource;
97+
import org.junit.jupiter.params.provider.MethodSource;
9698

9799
import java.io.IOException;
98100
import java.nio.charset.StandardCharsets;
@@ -101,6 +103,7 @@
101103
import java.util.Collections;
102104
import java.util.Date;
103105
import java.util.List;
106+
import java.util.stream.Stream;
104107

105108
import static org.apache.commons.lang3.StringUtils.isNotBlank;
106109
import static org.assertj.core.api.Assertions.assertThat;
@@ -4225,7 +4228,7 @@ public void testToListOfResourcesAndExcludeContainer_withSearchSetContainingDocu
42254228
RequestDetails requestDetails = new SystemRequestDetails();
42264229
requestDetails.setResourceName("Bundle");
42274230

4228-
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(searchSet, ourCtx);
4231+
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(searchSet, ourCtx, requestDetails);
42294232
assertEquals(1, resources.size());
42304233
assertTrue(resources.contains(bundle));
42314234
}
@@ -4242,12 +4245,46 @@ public void testToListOfResourcesAndExcludeContainer_withSearchSetContainingPati
42424245
RequestDetails requestDetails = new SystemRequestDetails();
42434246
requestDetails.setResourceName("Patient");
42444247

4245-
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(searchSet, ourCtx);
4248+
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(searchSet, ourCtx, requestDetails);
42464249
assertEquals(2, resources.size());
42474250
assertTrue(resources.contains(patient1));
42484251
assertTrue(resources.contains(patient2));
42494252
}
42504253

4254+
@ParameterizedTest
4255+
@MethodSource("provideArgumentsForToListOfResourcesAndExcludeContainerUnlessStandalone")
4256+
public void givenAsearchRequestWithOperationOutcomeIntheResponse_whenToListOfResourcesAndExcludeContainerUnlessStandalone_thenReturnNoOperationOutcome(
4257+
IBaseResource theResource, RequestDetails theRequestDetails, int theExpectedListSize
4258+
) {
4259+
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(theResource, ourCtx, theRequestDetails);
4260+
assertEquals(theExpectedListSize, resources.size());
4261+
}
4262+
4263+
private static Stream<Arguments> provideArgumentsForToListOfResourcesAndExcludeContainerUnlessStandalone() {
4264+
Stream.Builder<Arguments> retVal = Stream.builder();
4265+
AuthorizationInterceptor.REST_OPERATIONS_TO_EXCLUDE_SECURITY_FOR_OPERATION_OUTCOME.forEach((restOperationType)->{
4266+
RequestDetails firstRequestDetails = new SystemRequestDetails();
4267+
RequestDetails secondRequestDetails = new SystemRequestDetails();
4268+
firstRequestDetails.setResourceName("Patient");
4269+
firstRequestDetails.setRestOperationType(restOperationType);
4270+
secondRequestDetails.setResourceName("OperationOutcome");
4271+
secondRequestDetails.setRestOperationType(restOperationType);
4272+
4273+
OperationOutcome firstResponse = new OperationOutcome();
4274+
OperationOutcome secondResponse = new OperationOutcome();
4275+
firstResponse.addIssue().
4276+
setSeverity(OperationOutcome.IssueSeverity.INFORMATION).
4277+
setCode(OperationOutcome.IssueType.INFORMATIONAL);
4278+
secondResponse.addIssue().
4279+
setSeverity(OperationOutcome.IssueSeverity.ERROR)
4280+
.setCode(OperationOutcome.IssueType.CODEINVALID);
4281+
4282+
retVal.add(Arguments.of(firstResponse, firstRequestDetails, 0));
4283+
retVal.add(Arguments.of(secondResponse, secondRequestDetails, 1));
4284+
});
4285+
return retVal.build();
4286+
}
4287+
42514288
@ParameterizedTest
42524289
@EnumSource(value = Bundle.BundleType.class, names = {"DOCUMENT", "MESSAGE"})
42534290
public void testShouldExamineBundleResources_withBundleRequestAndStandAloneBundleType_returnsFalse(Bundle.BundleType theBundleType) {

0 commit comments

Comments
 (0)
Please sign in to comment.