Refactor to support arbitrary additional SPs based on Type for a compartment

This commit is contained in:
Tadgh 2021-09-14 01:02:19 -04:00
parent b2d6f75964
commit 3d24a4ce16
6 changed files with 162 additions and 5 deletions

View File

@ -704,6 +704,21 @@ public class FhirTerser {
* @throws IllegalArgumentException If theTarget does not contain both a resource type and ID * @throws IllegalArgumentException If theTarget does not contain both a resource type and ID
*/ */
public boolean isSourceInCompartmentForTarget(String theCompartmentName, IBaseResource theSource, IIdType theTarget) { public boolean isSourceInCompartmentForTarget(String theCompartmentName, IBaseResource theSource, IIdType theTarget) {
return isSourceInCompartmentForTarget(theCompartmentName, theSource, theTarget, null);
}
/**
* Returns <code>true</code> if <code>theSource</code> is in the compartment named <code>theCompartmentName</code>
* belonging to resource <code>theTarget</code>
*
* @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 <code>true</code> if <code>theSource</code> 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<String> theAdditionalCompartmentParamNames) {
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank"); Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank");
Validate.notNull(theSource, "theSource must not be null"); Validate.notNull(theSource, "theSource must not be null");
Validate.notNull(theTarget, "theTarget must not be null"); Validate.notNull(theTarget, "theTarget must not be null");
@ -720,6 +735,18 @@ public class FhirTerser {
} }
List<RuntimeSearchParam> params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName); List<RuntimeSearchParam> params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName);
//If passed an additional set of searchparameter names, add them for comparison purposes.
if (theAdditionalCompartmentParamNames != null) {
List<RuntimeSearchParam> 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 (RuntimeSearchParam nextParam : params) {
for (String nextPath : nextParam.getPathsSplit()) { for (String nextPath : nextParam.getPathsSplit()) {

View File

@ -22,7 +22,4 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
enum AppliesTypeEnum { enum AppliesTypeEnum {
ALL_RESOURCES, TYPES, INSTANCES ALL_RESOURCES, TYPES, INSTANCES
} }

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
*/ */
import java.util.Collection; import java.util.Collection;
import java.util.List;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
@ -58,6 +59,8 @@ public interface IAuthRuleBuilderRuleOpClassifier {
*/ */
IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, Collection<? extends IIdType> theOwners); IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, Collection<? extends IIdType> theOwners);
IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, IIdType theOwner, List<String> additionalTypeSearchParamNames);
/** /**
* Rule applies to any resource instances * Rule applies to any resource instances
* <p> * <p>

View File

@ -451,6 +451,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
private Collection<? extends IIdType> myInCompartmentOwners; private Collection<? extends IIdType> myInCompartmentOwners;
private Collection<IIdType> myAppliesToInstances; private Collection<IIdType> myAppliesToInstances;
private RuleImplOp myRule; private RuleImplOp myRule;
private List<String> myAdditionalSearchParamsForCompartmentTypes;
/** /**
* Constructor * Constructor
@ -483,6 +484,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
myRule.setClassifierCompartmentOwners(myInCompartmentOwners); myRule.setClassifierCompartmentOwners(myInCompartmentOwners);
myRule.setAppliesToDeleteCascade(myOnCascade); myRule.setAppliesToDeleteCascade(myOnCascade);
myRule.setAppliesToDeleteExpunge(myOnExpunge); myRule.setAppliesToDeleteExpunge(myOnExpunge);
myRule.setAdditionalSearchParamsForCompartmentTypes(myAdditionalSearchParamsForCompartmentTypes);
myRules.add(myRule); myRules.add(myRule);
return new RuleBuilderFinished(myRule); return new RuleBuilderFinished(myRule);
@ -519,6 +521,26 @@ public class RuleBuilder implements IAuthRuleBuilder {
return finished(); return finished();
} }
@Override
public IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, IIdType theOwner, List<String> 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<RuleImplOp> 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<RuleImplOp> findMatchingRule() { private Optional<RuleImplOp> findMatchingRule() {
return myRules.stream() return myRules.stream()
.filter(RuleImplOp.class::isInstance) .filter(RuleImplOp.class::isInstance)

View File

@ -18,6 +18,7 @@ import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.bundle.BundleEntryParts; import ca.uhn.fhir.util.bundle.BundleEntryParts;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils; 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.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseBundle;
@ -28,6 +29,8 @@ import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -69,6 +72,8 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
private Collection<IIdType> myAppliesToInstances; private Collection<IIdType> myAppliesToInstances;
private boolean myAppliesToDeleteCascade; private boolean myAppliesToDeleteCascade;
private boolean myAppliesToDeleteExpunge; private boolean myAppliesToDeleteExpunge;
private boolean myDeviceIncludedInPatientCompartment;
private Map<String, Set<String>> myAdditionalCompartmentSearchParamMap;
/** /**
* Constructor * Constructor
@ -337,7 +342,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
for (IIdType next : myClassifierCompartmentOwners) { for (IIdType next : myClassifierCompartmentOwners) {
if (target.resource != null) { if (target.resource != null) {
if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, target.resource, next)) {
Set<String> additionalSearchParamNames = null;
if (myAdditionalCompartmentSearchParamMap != null) {
additionalSearchParamNames = myAdditionalCompartmentSearchParamMap.get(target.resourceType);
}
if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, target.resource, next, additionalSearchParamNames)) {
foundMatch = true; foundMatch = true;
break; break;
} }
@ -372,6 +382,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
String compartmentOwnerResourceType = next.getResourceType(); String compartmentOwnerResourceType = next.getResourceType();
if (!StringUtils.equals(target.resourceType, compartmentOwnerResourceType)) { if (!StringUtils.equals(target.resourceType, compartmentOwnerResourceType)) {
List<RuntimeSearchParam> params = sourceDef.getSearchParamsForCompartmentName(compartmentOwnerResourceType); List<RuntimeSearchParam> 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()) { if (!params.isEmpty()) {
/* /*
@ -656,6 +672,10 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
myAppliesToDeleteExpunge = theAppliesToDeleteExpunge; myAppliesToDeleteExpunge = theAppliesToDeleteExpunge;
} }
void setDeviceIncludedInPatientCompartment(boolean theDeviceIncludedInPatientCompartment) {
myDeviceIncludedInPatientCompartment = theDeviceIncludedInPatientCompartment;
}
public void addClassifierCompartmentOwner(IIdType theOwner) { public void addClassifierCompartmentOwner(IIdType theOwner) {
List<IIdType> newList = new ArrayList<>(myClassifierCompartmentOwners); List<IIdType> newList = new ArrayList<>(myClassifierCompartmentOwners);
newList.add(theOwner); newList.add(theOwner);
@ -681,4 +701,17 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
return false; return false;
} }
} }
public void setAdditionalSearchParamsForCompartmentTypes(List<String> 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;
}
} }

View File

@ -376,7 +376,39 @@ public class AuthorizationInterceptorR4Test {
assertTrue(ourHitMethod); assertTrue(ourHitMethod);
} }
@Test
public void testDeviceIsPartOfPatientCompartment() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
List<String> 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 @Test
public void testAllowByCompartmentUsingUnqualifiedIds() throws Exception { public void testAllowByCompartmentUsingUnqualifiedIds() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@ -437,6 +469,24 @@ public class AuthorizationInterceptorR4Test {
extractResponseAndClose(status); extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode()); assertEquals(403, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod); 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<? extends IBaseResource> 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<Resource> search(
@OptionalParam(name = "patient") ReferenceParam thePatient
) {
ourHitMethod = true;
return ourReturn;
}
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static class DummyObservationResourceProvider implements IResourceProvider { public static class DummyObservationResourceProvider implements IResourceProvider {
@ -3953,12 +4027,13 @@ public class AuthorizationInterceptorR4Test {
DummyEncounterResourceProvider encProv = new DummyEncounterResourceProvider(); DummyEncounterResourceProvider encProv = new DummyEncounterResourceProvider();
DummyCarePlanResourceProvider cpProv = new DummyCarePlanResourceProvider(); DummyCarePlanResourceProvider cpProv = new DummyCarePlanResourceProvider();
DummyDiagnosticReportResourceProvider drProv = new DummyDiagnosticReportResourceProvider(); DummyDiagnosticReportResourceProvider drProv = new DummyDiagnosticReportResourceProvider();
DummyDeviceResourceProvider devProv = new DummyDeviceResourceProvider();
PlainProvider plainProvider = new PlainProvider(); PlainProvider plainProvider = new PlainProvider();
ServletHandler proxyHandler = new ServletHandler(); ServletHandler proxyHandler = new ServletHandler();
ourServlet = new RestfulServer(ourCtx); ourServlet = new RestfulServer(ourCtx);
ourServlet.setFhirContext(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 DummyServiceRequestResourceProvider());
ourServlet.registerProvider(new DummyConsentResourceProvider()); ourServlet.registerProvider(new DummyConsentResourceProvider());
ourServlet.setPlainProviders(plainProvider); ourServlet.setPlainProviders(plainProvider);