Add docs and changelog
This commit is contained in:
parent
3d24a4ce16
commit
75a16f3b13
|
@ -39,6 +39,7 @@ import org.hl7.fhir.dstu3.model.IdType;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
@ -205,6 +206,18 @@ public class AuthorizationInterceptors {
|
||||||
};
|
};
|
||||||
//END SNIPPET: bulkExport
|
//END SNIPPET: bulkExport
|
||||||
|
|
||||||
|
//START SNIPPET: advancedCompartment
|
||||||
|
new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||||
|
@Override
|
||||||
|
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||||
|
List<String> additionalCompartmentSpNames = Collections.singletonList("device:patient");
|
||||||
|
return new RuleBuilder()
|
||||||
|
.allow().read().allResources().inCompartmentWithAdditionalSearchParams("Patient", new IdType("Patient/123"), additionalCompartmentSpNames)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//END SNIPPET: advancedCompartment
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
jira: SMILE-1118
|
||||||
|
title: "Add new RuleBuilder options which allow you to specify additional resources and search parameters which match a given compartment. More explanations of
|
||||||
|
the enhancements can be found in [the documentation](/hapi-fhir/docs/security/authorization_interceptor.html#advanced-compartment-authorization)."
|
|
@ -83,3 +83,14 @@ AuthorizationInterceptor can be used to provide nuanced control over the kinds o
|
||||||
```java
|
```java
|
||||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|bulkExport}}
|
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|bulkExport}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Advanced Compartment authorization
|
||||||
|
|
||||||
|
AuthorizationInterceptor can be used to provide fine-grained control over compartment reads and writes as well. There is a strict FHIR definition
|
||||||
|
of which resources and related search parameters fall into a given compartment. However, sometimes the defaults do not suffice. The following is an example
|
||||||
|
of an R4 ruleset which allows `device.patient` to be considered in the Patient compartment, on top of all the standard search parameters.
|
||||||
|
|
||||||
|
|
||||||
|
```java
|
||||||
|
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|advancedCompartment}}
|
||||||
|
```
|
||||||
|
|
|
@ -43,6 +43,28 @@ public interface IAuthRuleBuilderRuleOpClassifier {
|
||||||
*/
|
*/
|
||||||
IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, IIdType theOwner);
|
IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, IIdType theOwner);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rule applies to resources in the given compartment.
|
||||||
|
* <p>
|
||||||
|
* For example, to apply the rule to any observations in the patient compartment
|
||||||
|
* belonging to patient "123", you would invoke this with</br>
|
||||||
|
* <code>inCompartment("Patient", new IdType("Patient", "123"))</code>
|
||||||
|
*
|
||||||
|
* This call also allows you to pass additional search parameters that count as being included in the given compartment,
|
||||||
|
* passed in as a list of `resourceType:search-parameter-name`. For example, if you select a compartment name of "patient",
|
||||||
|
* you could pass in a singleton list consisting of the string "device:patient", which would cause any devices belonging
|
||||||
|
* to the patient to be permitted by the authorization rule.
|
||||||
|
*
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* This call completes the rule and adds the rule to the chain.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param theCompartmentName The name of the compartment (must not be null or blank)
|
||||||
|
* @param theOwner The owner of the compartment. Note that both the resource type and ID must be populated in this ID.
|
||||||
|
* @param theAdditionalTypeSearchParamNames A list of strings for additional resource types and search parameters which count as being in the compartment, in the form "resourcetype:search-parameter-name".
|
||||||
|
*/
|
||||||
|
IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, IIdType theOwner, List<String> theAdditionalTypeSearchParamNames);
|
||||||
/**
|
/**
|
||||||
* Rule applies to resources in the given compartment.
|
* Rule applies to resources in the given compartment.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -59,7 +81,31 @@ 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 resources in the given compartment.
|
||||||
|
* <p>
|
||||||
|
* For example, to apply the rule to any observations in the patient compartment
|
||||||
|
* belonging to patient "123", you would invoke this with</br>
|
||||||
|
* <code>inCompartment("Patient", new IdType("Patient", "123"))</code>
|
||||||
|
*
|
||||||
|
* This call also allows you to pass additional search parameters that count as being included in the given compartment,
|
||||||
|
* passed in as a list of `resourceType:search-parameter-name`. For example, if you select a compartment name of "patient",
|
||||||
|
* you could pass in a singleton list consisting of the string "device:patient", which would cause any devices belonging
|
||||||
|
* to the patient to be permitted by the authorization rule.
|
||||||
|
*
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* This call completes the rule and adds the rule to the chain.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param theCompartmentName The name of the compartment (must not be null or blank)
|
||||||
|
* @param theOwners The owners of the compartment. Note that both the resource type and ID must be populated in these IDs.
|
||||||
|
* @param theAdditionalTypeSearchParamNames A list of strings for additional resource types and search parameters which count as being in the compartment, in the form "resourcetype:search-parameter-name".
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, Collection<? extends IIdType> theOwners, List<String> theAdditionalTypeSearchParamNames);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rule applies to any resource instances
|
* Rule applies to any resource instances
|
||||||
|
|
|
@ -492,6 +492,11 @@ public class RuleBuilder implements IAuthRuleBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, Collection<? extends IIdType> theOwners) {
|
public IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, Collection<? extends IIdType> theOwners) {
|
||||||
|
return inCompartmentWithAdditionalSearchParams(theCompartmentName, theOwners, new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, Collection<? extends IIdType> theOwners, List<String> theAdditionalTypeSearchParamNames) {
|
||||||
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null");
|
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null");
|
||||||
Validate.notNull(theOwners, "theOwners must not be null");
|
Validate.notNull(theOwners, "theOwners must not be null");
|
||||||
Validate.noNullElements(theOwners, "theOwners must not contain any null elements");
|
Validate.noNullElements(theOwners, "theOwners must not contain any null elements");
|
||||||
|
@ -500,39 +505,28 @@ public class RuleBuilder implements IAuthRuleBuilder {
|
||||||
}
|
}
|
||||||
myInCompartmentName = theCompartmentName;
|
myInCompartmentName = theCompartmentName;
|
||||||
myInCompartmentOwners = theOwners;
|
myInCompartmentOwners = theOwners;
|
||||||
|
myAdditionalSearchParamsForCompartmentTypes = theAdditionalTypeSearchParamNames;
|
||||||
myClassifierType = ClassifierTypeEnum.IN_COMPARTMENT;
|
myClassifierType = ClassifierTypeEnum.IN_COMPARTMENT;
|
||||||
return finished();
|
return finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, IIdType theOwner) {
|
public IAuthRuleBuilderRuleOpClassifierFinished inCompartment(String theCompartmentName, IIdType theOwner) {
|
||||||
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null");
|
return inCompartmentWithAdditionalSearchParams(theCompartmentName, theOwner, new ArrayList<>());
|
||||||
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.addClassifierCompartmentOwner(theOwner);
|
|
||||||
return new RuleBuilderFinished(rule);
|
|
||||||
}
|
|
||||||
myInCompartmentOwners = Collections.singletonList(theOwner);
|
|
||||||
return finished();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, IIdType theOwner, List<String> additionalTypeSearchParamNames) {
|
public IAuthRuleBuilderRuleOpClassifierFinished inCompartmentWithAdditionalSearchParams(String theCompartmentName, IIdType theOwner, List<String> theAdditionalTypeSearchParamNames) {
|
||||||
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null");
|
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null");
|
||||||
Validate.notNull(theOwner, "theOwner must not be null");
|
Validate.notNull(theOwner, "theOwner must not be null");
|
||||||
validateOwner(theOwner);
|
validateOwner(theOwner);
|
||||||
myClassifierType = ClassifierTypeEnum.IN_COMPARTMENT;
|
myClassifierType = ClassifierTypeEnum.IN_COMPARTMENT;
|
||||||
myInCompartmentName = theCompartmentName;
|
myInCompartmentName = theCompartmentName;
|
||||||
|
myAdditionalSearchParamsForCompartmentTypes = theAdditionalTypeSearchParamNames;
|
||||||
Optional<RuleImplOp> oRule = findMatchingRule();
|
Optional<RuleImplOp> oRule = findMatchingRule();
|
||||||
|
|
||||||
if (oRule.isPresent()) {
|
if (oRule.isPresent()) {
|
||||||
RuleImplOp rule = oRule.get();
|
RuleImplOp rule = oRule.get();
|
||||||
rule.setAdditionalSearchParamsForCompartmentTypes(additionalTypeSearchParamNames);
|
rule.setAdditionalSearchParamsForCompartmentTypes(myAdditionalSearchParamsForCompartmentTypes);
|
||||||
rule.addClassifierCompartmentOwner(theOwner);
|
rule.addClassifierCompartmentOwner(theOwner);
|
||||||
return new RuleBuilderFinished(rule);
|
return new RuleBuilderFinished(rule);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ 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;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
@ -345,7 +346,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
||||||
|
|
||||||
Set<String> additionalSearchParamNames = null;
|
Set<String> additionalSearchParamNames = null;
|
||||||
if (myAdditionalCompartmentSearchParamMap != null) {
|
if (myAdditionalCompartmentSearchParamMap != null) {
|
||||||
additionalSearchParamNames = myAdditionalCompartmentSearchParamMap.get(target.resourceType);
|
additionalSearchParamNames = myAdditionalCompartmentSearchParamMap.get(ctx.getResourceType(target.resource).toLowerCase());
|
||||||
}
|
}
|
||||||
if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, target.resource, next, additionalSearchParamNames)) {
|
if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, target.resource, next, additionalSearchParamNames)) {
|
||||||
foundMatch = true;
|
foundMatch = true;
|
||||||
|
@ -381,13 +382,17 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
||||||
RuntimeResourceDefinition sourceDef = theRequestDetails.getFhirContext().getResourceDefinition(target.resourceType);
|
RuntimeResourceDefinition sourceDef = theRequestDetails.getFhirContext().getResourceDefinition(target.resourceType);
|
||||||
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) {
|
|
||||||
|
Set<String> additionalParamNames = myAdditionalCompartmentSearchParamMap.get(sourceDef.getName().toLowerCase());
|
||||||
|
List<RuntimeSearchParam> additionalParams = additionalParamNames.stream().map(paramName -> sourceDef.getSearchParam(paramName)).collect(Collectors.toList());
|
||||||
if (params == null || params.isEmpty()) {
|
if (params == null || params.isEmpty()) {
|
||||||
params = new ArrayList<>();
|
params = additionalParams;
|
||||||
}
|
} else {
|
||||||
params.add(sourceDef.getSearchParam("patient"));
|
params.addAll(additionalParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.isEmpty()) {
|
if (!params.isEmpty()) {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -708,10 +713,9 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String typeAndParam: theTypeAndParams) {
|
for (String typeAndParam: theTypeAndParams) {
|
||||||
String[] split = typeAndParam.split(",");
|
String[] split = typeAndParam.split(":");
|
||||||
Validate.isTrue(split.length == 2);
|
Validate.isTrue(split.length == 2);
|
||||||
myAdditionalCompartmentSearchParamMap.computeIfAbsent(split[0], (v) -> new HashSet<>()).add(split[1]);
|
myAdditionalCompartmentSearchParamMap.computeIfAbsent(split[0].toLowerCase(), (v) -> new HashSet<>()).add(split[1].toLowerCase());
|
||||||
}
|
}
|
||||||
this.myAdditionalCompartmentSearchParamMap = myAdditionalCompartmentSearchParamMap;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -376,15 +376,19 @@ public class AuthorizationInterceptorR4Test {
|
||||||
assertTrue(ourHitMethod);
|
assertTrue(ourHitMethod);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDeviceIsPartOfPatientCompartment() throws Exception {
|
public void testCustomCompartmentSpsOnMultipleInstances() throws Exception {
|
||||||
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||||
@Override
|
@Override
|
||||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||||
List<String> bonusPatientCompartmentSearchParams = Collections.singletonList("device:patient");
|
List<String> bonusPatientCompartmentSearchParams = Collections.singletonList("device:patient");
|
||||||
|
List<IdType> relatedIds = new ArrayList<>();
|
||||||
|
relatedIds.add(new IdType("Patient/123"));
|
||||||
|
relatedIds.add(new IdType("Patient/456"));
|
||||||
return new RuleBuilder()
|
return new RuleBuilder()
|
||||||
.allow().read().allResources()
|
.allow().read().allResources()
|
||||||
.inCompartmentWithAdditionalSearchParams("Patient", new IdType("Patient/123"), bonusPatientCompartmentSearchParams)
|
.inCompartmentWithAdditionalSearchParams("Patient", relatedIds, bonusPatientCompartmentSearchParams)
|
||||||
.andThen().denyAll()
|
.andThen().denyAll()
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue