diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java index b0dca947658..34d5db49c5c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java @@ -704,6 +704,21 @@ public class FhirTerser { * @throws IllegalArgumentException If theTarget does not contain both a resource type and ID */ public boolean isSourceInCompartmentForTarget(String theCompartmentName, IBaseResource theSource, IIdType theTarget) { + return isSourceInCompartmentForTarget(theCompartmentName, theSource, theTarget, null); + } + + /** + * Returns true if theSource is in the compartment named theCompartmentName + * belonging to resource theTarget + * + * @param theCompartmentName The name of the compartment + * @param theSource The potential member of the compartment + * @param theTarget The owner of the compartment. Note that both the resource type and ID must be filled in on this IIdType or the method will throw an {@link IllegalArgumentException} + * @param theAdditionalCompartmentParamNames If provided, search param names provided here will be considered as included in the given compartment for this comparison. + * @return true if theSource is in the compartment or one of the additional parameters matched. + * @throws IllegalArgumentException If theTarget does not contain both a resource type and ID + */ + public boolean isSourceInCompartmentForTarget(String theCompartmentName, IBaseResource theSource, IIdType theTarget, Set theAdditionalCompartmentParamNames) { Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank"); Validate.notNull(theSource, "theSource must not be null"); Validate.notNull(theTarget, "theTarget must not be null"); @@ -720,6 +735,18 @@ public class FhirTerser { } List params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName); + + //If passed an additional set of searchparameter names, add them for comparison purposes. + if (theAdditionalCompartmentParamNames != null) { + List additionalParams = theAdditionalCompartmentParamNames.stream().map(paramName -> sourceDef.getSearchParam(paramName)).collect(Collectors.toList()); + if (params == null || params.isEmpty()) { + params = additionalParams; + } else { + params.addAll(additionalParams); + } + } + + for (RuntimeSearchParam nextParam : params) { for (String nextPath : nextParam.getPathsSplit()) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AppliesTypeEnum.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AppliesTypeEnum.java index d3df71d8687..a878f0fb5de 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AppliesTypeEnum.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AppliesTypeEnum.java @@ -22,7 +22,4 @@ package ca.uhn.fhir.rest.server.interceptor.auth; enum AppliesTypeEnum { ALL_RESOURCES, TYPES, INSTANCES - - - } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifier.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifier.java index 6c3ceaab9a9..b748fddac74 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifier.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifier.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; */ import java.util.Collection; +import java.util.List; import org.hl7.fhir.instance.model.api.IIdType; @@ -58,6 +59,8 @@ public interface IAuthRuleBuilderRuleOpClassifier { */ IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, Collection theOwners); + IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, IIdType theOwner, List additionalTypeSearchParamNames); + /** * Rule applies to any resource instances *

diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 086797a8ef7..4e1c9b8ecd3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -451,6 +451,7 @@ public class RuleBuilder implements IAuthRuleBuilder { private Collection myInCompartmentOwners; private Collection myAppliesToInstances; private RuleImplOp myRule; + private List myAdditionalSearchParamsForCompartmentTypes; /** * Constructor @@ -483,6 +484,7 @@ public class RuleBuilder implements IAuthRuleBuilder { myRule.setClassifierCompartmentOwners(myInCompartmentOwners); myRule.setAppliesToDeleteCascade(myOnCascade); myRule.setAppliesToDeleteExpunge(myOnExpunge); + myRule.setAdditionalSearchParamsForCompartmentTypes(myAdditionalSearchParamsForCompartmentTypes); myRules.add(myRule); return new RuleBuilderFinished(myRule); @@ -519,6 +521,26 @@ public class RuleBuilder implements IAuthRuleBuilder { return finished(); } + @Override + public IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, IIdType theOwner, List additionalTypeSearchParamNames) { + Validate.notBlank(theCompartmentName, "theCompartmentName must not be null"); + Validate.notNull(theOwner, "theOwner must not be null"); + validateOwner(theOwner); + myClassifierType = ClassifierTypeEnum.IN_COMPARTMENT; + myInCompartmentName = theCompartmentName; + Optional oRule = findMatchingRule(); + + if (oRule.isPresent()) { + RuleImplOp rule = oRule.get(); + rule.setAdditionalSearchParamsForCompartmentTypes(additionalTypeSearchParamNames); + rule.addClassifierCompartmentOwner(theOwner); + return new RuleBuilderFinished(rule); + } + myInCompartmentOwners = Collections.singletonList(theOwner); + return finished(); + } + + private Optional findMatchingRule() { return myRules.stream() .filter(RuleImplOp.class::isInstance) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index 0810235f2ac..73ccec7d447 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.bundle.BundleEntryParts; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBaseBundle; @@ -28,6 +29,8 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -69,6 +72,8 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { private Collection myAppliesToInstances; private boolean myAppliesToDeleteCascade; private boolean myAppliesToDeleteExpunge; + private boolean myDeviceIncludedInPatientCompartment; + private Map> myAdditionalCompartmentSearchParamMap; /** * Constructor @@ -337,7 +342,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { for (IIdType next : myClassifierCompartmentOwners) { if (target.resource != null) { - if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, target.resource, next)) { + + Set additionalSearchParamNames = null; + if (myAdditionalCompartmentSearchParamMap != null) { + additionalSearchParamNames = myAdditionalCompartmentSearchParamMap.get(target.resourceType); + } + if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, target.resource, next, additionalSearchParamNames)) { foundMatch = true; break; } @@ -372,6 +382,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { String compartmentOwnerResourceType = next.getResourceType(); if (!StringUtils.equals(target.resourceType, compartmentOwnerResourceType)) { List params = sourceDef.getSearchParamsForCompartmentName(compartmentOwnerResourceType); + if (target.resourceType.equalsIgnoreCase("Device") && myDeviceIncludedInPatientCompartment) { + if (params == null || params.isEmpty()) { + params = new ArrayList<>(); + } + params.add(sourceDef.getSearchParam("patient")); + } if (!params.isEmpty()) { /* @@ -656,6 +672,10 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { myAppliesToDeleteExpunge = theAppliesToDeleteExpunge; } + void setDeviceIncludedInPatientCompartment(boolean theDeviceIncludedInPatientCompartment) { + myDeviceIncludedInPatientCompartment = theDeviceIncludedInPatientCompartment; + } + public void addClassifierCompartmentOwner(IIdType theOwner) { List newList = new ArrayList<>(myClassifierCompartmentOwners); newList.add(theOwner); @@ -681,4 +701,17 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { return false; } } + + public void setAdditionalSearchParamsForCompartmentTypes(List theTypeAndParams) { + if (myAdditionalCompartmentSearchParamMap == null) { + myAdditionalCompartmentSearchParamMap = new HashMap<>(); + } + + for (String typeAndParam: theTypeAndParams) { + String[] split = typeAndParam.split(","); + Validate.isTrue(split.length == 2); + myAdditionalCompartmentSearchParamMap.computeIfAbsent(split[0], (v) -> new HashSet<>()).add(split[1]); + } + this.myAdditionalCompartmentSearchParamMap = myAdditionalCompartmentSearchParamMap; + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java index 3bb305e4211..958005cfa4a 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java @@ -376,7 +376,39 @@ public class AuthorizationInterceptorR4Test { assertTrue(ourHitMethod); } + @Test + public void testDeviceIsPartOfPatientCompartment() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + List bonusPatientCompartmentSearchParams = Collections.singletonList("device:patient"); + return new RuleBuilder() + .allow().read().allResources() + .inCompartmentWithAdditionalSearchParams("Patient", new IdType("Patient/123"), bonusPatientCompartmentSearchParams) + .andThen().denyAll() + .build(); + } + }); + HttpGet httpGet; + HttpResponse status; + + Patient patient; + + + patient = new Patient(); + patient.setId("Patient/123"); + Device d = new Device(); + d.getPatient().setResource(patient); + + ourHitMethod = false; + ourReturn = Collections.singletonList(d); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Device/124456"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + } @Test public void testAllowByCompartmentUsingUnqualifiedIds() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -437,6 +469,24 @@ public class AuthorizationInterceptorR4Test { extractResponseAndClose(status); assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); + + + patient = new Patient(); + patient.setId("Patient/123"); + carePlan = new CarePlan(); + carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE); + carePlan.getSubject().setResource(patient); + + Device d = new Device(); + d.getPatient().setResource(patient); + + ourHitMethod = false; + ourReturn = Collections.singletonList(d); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Device/123456"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); } /** @@ -3619,6 +3669,30 @@ public class AuthorizationInterceptorR4Test { } } + public static class DummyDeviceResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Device.class; + } + + @Read(version = true) + public Device read(@IdParam IdType theId) { + ourHitMethod = true; + if (ourReturn.isEmpty()) { + throw new ResourceNotFoundException(theId); + } + return (Device) ourReturn.get(0); + } + @Search() + public List search( + @OptionalParam(name = "patient") ReferenceParam thePatient + ) { + ourHitMethod = true; + return ourReturn; + } + } + @SuppressWarnings("unused") public static class DummyObservationResourceProvider implements IResourceProvider { @@ -3953,12 +4027,13 @@ public class AuthorizationInterceptorR4Test { DummyEncounterResourceProvider encProv = new DummyEncounterResourceProvider(); DummyCarePlanResourceProvider cpProv = new DummyCarePlanResourceProvider(); DummyDiagnosticReportResourceProvider drProv = new DummyDiagnosticReportResourceProvider(); + DummyDeviceResourceProvider devProv = new DummyDeviceResourceProvider(); PlainProvider plainProvider = new PlainProvider(); ServletHandler proxyHandler = new ServletHandler(); ourServlet = new RestfulServer(ourCtx); ourServlet.setFhirContext(ourCtx); - ourServlet.registerProviders(patProvider, obsProv, encProv, cpProv, orgProv, drProv); + ourServlet.registerProviders(patProvider, obsProv, encProv, cpProv, orgProv, drProv, devProv); ourServlet.registerProvider(new DummyServiceRequestResourceProvider()); ourServlet.registerProvider(new DummyConsentResourceProvider()); ourServlet.setPlainProviders(plainProvider);