Add support for patch in AuthorizationInterceptor

This commit is contained in:
James Agnew 2018-07-01 11:46:11 -04:00
parent bbf47454b3
commit 495fd9f68e
16 changed files with 416 additions and 205 deletions

View File

@ -4,6 +4,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.List;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.model.dstu2.resource.Patient;
@ -140,5 +141,21 @@ public class AuthorizationInterceptors {
}
};
//END SNIPPET: authorizeTenantAction
//START SNIPPET: patchAll
new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
// Authorize patch requests
.allow().patch().allRequests().andThen()
// Authorize actual writes that patch may perform
.allow().write().allResources().inCompartment("Patient", new IdType("Patient/123")).andThen()
.build();
}
};
//END SNIPPET: patchAll
}
}

View File

@ -40,64 +40,6 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
unregisterInterceptors();
}
/**
* See #778
*/
@Test
public void testReadingObservationAccessRight() {
Practitioner practitioner1 = new Practitioner();
final IIdType practitionerId1 = myClient.create().resource(practitioner1).execute().getId().toUnqualifiedVersionless();
Practitioner practitioner2 = new Practitioner();
final IIdType practitionerId2 = myClient.create().resource(practitioner2).execute().getId().toUnqualifiedVersionless();
Patient patient = new Patient();
patient.setActive(true);
final IIdType patientId = myClient.create().resource(patient).execute().getId().toUnqualifiedVersionless();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
// allow write all Observation resource
// allow read only Observation resource in which it has a practitioner1 or practitioner2 compartment
return new RuleBuilder().allow()
.write()
.resourcesOfType(Observation.class)
.withAnyId()
.andThen()
.allow()
.read()
.resourcesOfType(Observation.class)
.inCompartment("Practitioner", Arrays.asList(practitionerId1, practitionerId2))
.andThen()
.denyAll()
.build();
}
});
Observation obs1 = new Observation();
obs1.setStatus(ObservationStatus.FINAL);
obs1.setPerformer(
Arrays.asList(new Reference(practitionerId1), new Reference(practitionerId2)));
IIdType oid1 = myClient.create().resource(obs1).execute().getId().toUnqualified();
// Observation with practitioner1 and practitioner1 as the Performer -> should have the read access
myClient.read().resource(Observation.class).withId(oid1).execute();
Observation obs2 = new Observation();
obs2.setStatus(ObservationStatus.FINAL);
obs2.setSubject(new Reference(patientId));
IIdType oid2 = myClient.create().resource(obs2).execute().getId().toUnqualified();
// Observation with patient as the subject -> read access should be blocked
try {
myClient.read().resource(Observation.class).withId(oid2).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
/**
* See #667
*/
@ -455,13 +397,11 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
});
// Create a bundle that will be used as a transaction
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
String encounterId = "123-123";
String encounterSystem = "http://our.internal.code.system/encounter";
Encounter encounter = new Encounter();
@ -523,8 +463,117 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
}
@Test
public void testPatchWithinCompartment() {
Patient pt1 = new Patient();
pt1.setActive(true);
final IIdType pid1 = myClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless();
Observation obs1 = new Observation();
obs1.setStatus(ObservationStatus.FINAL);
obs1.setSubject(new Reference(pid1));
IIdType oid1 = myClient.create().resource(obs1).execute().getId().toUnqualified();
Patient pt2 = new Patient();
pt2.setActive(false);
final IIdType pid2 = myClient.create().resource(pt2).execute().getId().toUnqualifiedVersionless();
Observation obs2 = new Observation();
obs2.setStatus(ObservationStatus.FINAL);
obs2.setSubject(new Reference(pid2));
IIdType oid2 = myClient.create().resource(obs2).execute().getId().toUnqualified();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().patch().allRequests().andThen()
.allow().write().allResources().inCompartment("Patient", pid1).andThen()
.allow().read().allResources().withAnyId().andThen()
.build();
}
});
String patchBody = "[\n" +
" { \"op\": \"replace\", \"path\": \"Observation/status\", \"value\": \"amended\" }\n" +
" ]";
// Allowed
myClient.patch().withBody(patchBody).withId(oid1).execute();
obs1 = myClient.read().resource(Observation.class).withId(oid1.toUnqualifiedVersionless()).execute();
assertEquals(ObservationStatus.AMENDED, obs1.getStatus());
// Denied
try {
myClient.patch().withBody(patchBody).withId(oid2).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
obs2 = myClient.read().resource(Observation.class).withId(oid2.toUnqualifiedVersionless()).execute();
assertEquals(ObservationStatus.FINAL, obs2.getStatus());
}
/**
* See #778
*/
@Test
public void testReadingObservationAccessRight() {
Practitioner practitioner1 = new Practitioner();
final IIdType practitionerId1 = myClient.create().resource(practitioner1).execute().getId().toUnqualifiedVersionless();
Practitioner practitioner2 = new Practitioner();
final IIdType practitionerId2 = myClient.create().resource(practitioner2).execute().getId().toUnqualifiedVersionless();
Patient patient = new Patient();
patient.setActive(true);
final IIdType patientId = myClient.create().resource(patient).execute().getId().toUnqualifiedVersionless();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
// allow write all Observation resource
// allow read only Observation resource in which it has a practitioner1 or practitioner2 compartment
return new RuleBuilder().allow()
.write()
.resourcesOfType(Observation.class)
.withAnyId()
.andThen()
.allow()
.read()
.resourcesOfType(Observation.class)
.inCompartment("Practitioner", Arrays.asList(practitionerId1, practitionerId2))
.andThen()
.denyAll()
.build();
}
});
Observation obs1 = new Observation();
obs1.setStatus(ObservationStatus.FINAL);
obs1.setPerformer(
Arrays.asList(new Reference(practitionerId1), new Reference(practitionerId2)));
IIdType oid1 = myClient.create().resource(obs1).execute().getId().toUnqualified();
// Observation with practitioner1 and practitioner1 as the Performer -> should have the read access
myClient.read().resource(Observation.class).withId(oid1).execute();
Observation obs2 = new Observation();
obs2.setStatus(ObservationStatus.FINAL);
obs2.setSubject(new Reference(patientId));
IIdType oid2 = myClient.create().resource(obs2).execute().getId().toUnqualified();
// Observation with patient as the subject -> read access should be blocked
try {
myClient.read().resource(Observation.class).withId(oid2).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
private void unregisterInterceptors() {
for (IServerInterceptor next : new ArrayList<IServerInterceptor>(ourRestServer.getInterceptors())) {
for (IServerInterceptor next : new ArrayList<>(ourRestServer.getInterceptors())) {
if (next instanceof AuthorizationInterceptor) {
ourRestServer.unregisterInterceptor(next);
}

View File

@ -50,6 +50,7 @@ public enum AuthorizationFlagsEnum {
* version, this flag was the default and there was no ability to
* proactively block compartment read access.
*/
NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS;
NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS,
ALLOW_PATCH_REQUEST_UNCHALLENGED;
}

View File

@ -126,7 +126,7 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
* @param theRequestDetails The individual request currently being applied
*/
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new ArrayList<IAuthRule>();
return new ArrayList<>();
}
private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation, IBaseResource theRequestResource) {
@ -407,7 +407,9 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
private final IAuthRule myDecidingRule;
private final PolicyEnum myDecision;
public Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) {
Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) {
Validate.notNull(theDecision);
myDecision = theDecision;
myDecidingRule = theDecidingRule;
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -35,6 +35,7 @@ abstract class BaseRule implements IAuthRule {
private String myName;
private PolicyEnum myMode;
private List<IAuthRuleTester> myTesters;
private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker;
BaseRule(String theRuleName) {
myName = theRuleName;
@ -51,7 +52,7 @@ abstract class BaseRule implements IAuthRule {
public void addTesters(List<IAuthRuleTester> theTesters) {
theTesters.forEach(this::addTester);
}
boolean applyTesters(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource, IBaseResource theOutputResource) {
boolean retVal = true;
if (theOutputResource == null) {
@ -69,8 +70,9 @@ abstract class BaseRule implements IAuthRule {
return myMode;
}
void setMode(PolicyEnum theRuleMode) {
BaseRule setMode(PolicyEnum theRuleMode) {
myMode = theRuleMode;
return this;
}
@Override
@ -78,6 +80,14 @@ abstract class BaseRule implements IAuthRule {
return myName;
}
public RuleBuilder.ITenantApplicabilityChecker getTenantApplicabilityChecker() {
return myTenantApplicabilityChecker;
}
public final void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
}
public List<IAuthRuleTester> getTesters() {
if (myTesters == null) {
return Collections.emptyList();
@ -85,6 +95,16 @@ abstract class BaseRule implements IAuthRule {
return Collections.unmodifiableList(myTesters);
}
public boolean isOtherTenant(RequestDetails theRequestDetails) {
boolean otherTenant = false;
if (getTenantApplicabilityChecker() != null) {
if (!getTenantApplicabilityChecker().applies(theRequestDetails)) {
otherTenant = true;
}
}
return otherTenant;
}
Verdict newVerdict() {
return new Verdict(myMode, this);
}

View File

@ -0,0 +1,20 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IAuthRuleBuilderPatch {
/**
* With this setting, all <a href="http://hl7.org/fhir/http.html#patch">patch</a> requests will be permitted
* to proceed. This rule will not permit the
* {@link ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor#resourceCreated(RequestDetails, IBaseResource)}
* and
* {@link ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor#resourceUpdated(RequestDetails, IBaseResource, IBaseResource)}
* methods if your server supports {@link ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor}.
* In that case, additional rules are generally required in order to
* permit write operations.
*/
IAuthRuleFinished allRequests();
}

View File

@ -69,6 +69,11 @@ public interface IAuthRuleBuilderRule {
*/
IAuthRuleBuilderOperation operation();
/**
* This rule applies to a FHIR patch operation
*/
IAuthRuleBuilderPatch patch();
/**
* This rule applies to any FHIR operation involving reading, including
* <code>read</code>, <code>vread</code>, <code>search</code>, and

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -33,7 +33,6 @@ import java.util.Set;
class OperationRule extends BaseRule implements IAuthRule {
private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker;
private String myOperationName;
private boolean myAppliesToServer;
private HashSet<Class<? extends IBaseResource>> myAppliesToTypes;
@ -43,35 +42,35 @@ class OperationRule extends BaseRule implements IAuthRule {
private boolean myAppliesToAnyInstance;
private boolean myAppliesAtAnyLevel;
public OperationRule(String theRuleName) {
OperationRule(String theRuleName) {
super(theRuleName);
}
public void appliesAtAnyLevel(boolean theAppliesAtAnyLevel) {
void appliesAtAnyLevel(boolean theAppliesAtAnyLevel) {
myAppliesAtAnyLevel = theAppliesAtAnyLevel;
}
public void appliesToAnyInstance() {
void appliesToAnyInstance() {
myAppliesToAnyInstance = true;
}
public void appliesToAnyType() {
void appliesToAnyType() {
myAppliesToAnyType = true;
}
public void appliesToInstances(List<IIdType> theAppliesToIds) {
void appliesToInstances(List<IIdType> theAppliesToIds) {
myAppliesToIds = theAppliesToIds;
}
public void appliesToInstancesOfType(HashSet<Class<? extends IBaseResource>> theAppliesToTypes) {
void appliesToInstancesOfType(HashSet<Class<? extends IBaseResource>> theAppliesToTypes) {
myAppliesToInstancesOfType = theAppliesToTypes;
}
public void appliesToServer() {
void appliesToServer() {
myAppliesToServer = true;
}
public void appliesToTypes(HashSet<Class<? extends IBaseResource>> theAppliesToTypes) {
void appliesToTypes(HashSet<Class<? extends IBaseResource>> theAppliesToTypes) {
myAppliesToTypes = theAppliesToTypes;
}
@ -79,10 +78,8 @@ class OperationRule extends BaseRule implements IAuthRule {
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags) {
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
if (myTenantApplicabilityChecker != null) {
if (!myTenantApplicabilityChecker.applies(theRequestDetails)) {
return null;
}
if (isOtherTenant(theRequestDetails)) {
return null;
}
boolean applies = false;
@ -203,8 +200,4 @@ class OperationRule extends BaseRule implements IAuthRule {
myOperationName = theOperationName;
}
public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
}
}

View File

@ -33,7 +33,7 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
public class RuleBuilder implements IAuthRuleBuilder {
public static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private ArrayList<IAuthRule> myRules;
public RuleBuilder() {
@ -95,19 +95,12 @@ public class RuleBuilder implements IAuthRuleBuilder {
private class RuleBuilderFinished implements IAuthRuleFinished, IAuthRuleBuilderRuleOpClassifierFinished, IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId {
private final RuleImplOp myOpRule;
private final OperationRule myOperationRule;
protected ITenantApplicabilityChecker myTenantApplicabilityChecker;
private final BaseRule myOpRule;
ITenantApplicabilityChecker myTenantApplicabilityChecker;
private List<IAuthRuleTester> myTesters;
RuleBuilderFinished(RuleImplOp theRule) {
RuleBuilderFinished(BaseRule theRule) {
myOpRule = theRule;
myOperationRule = null;
}
public RuleBuilderFinished(OperationRule theRule) {
myOpRule = null;
myOperationRule = theRule;
}
@Override
@ -136,16 +129,11 @@ public class RuleBuilder implements IAuthRuleBuilder {
@Override
public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId forTenantIds(final Collection<String> theTenantIds) {
setTenantApplicabilityChecker(new ITenantApplicabilityChecker() {
@Override
public boolean applies(RequestDetails theRequest) {
return theTenantIds.contains(theRequest.getTenantId());
}
});
setTenantApplicabilityChecker(theRequest -> theTenantIds.contains(theRequest.getTenantId()));
return this;
}
public List<IAuthRuleTester> getTesters() {
List<IAuthRuleTester> getTesters() {
if (myTesters == null) {
return Collections.emptyList();
}
@ -159,23 +147,13 @@ public class RuleBuilder implements IAuthRuleBuilder {
@Override
public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId notForTenantIds(final Collection<String> theTenantIds) {
setTenantApplicabilityChecker(new ITenantApplicabilityChecker() {
@Override
public boolean applies(RequestDetails theRequest) {
return !theTenantIds.contains(theRequest.getTenantId());
}
});
setTenantApplicabilityChecker(theRequest -> !theTenantIds.contains(theRequest.getTenantId()));
return this;
}
private void setTenantApplicabilityChecker(ITenantApplicabilityChecker theTenantApplicabilityChecker) {
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
if (myOpRule != null) {
myOpRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker);
}
if (myOperationRule != null) {
myOperationRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker);
}
}
@Override
@ -184,12 +162,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
myTesters = new ArrayList<>();
}
myTesters.add(theTester);
if (myOperationRule != null) {
myOperationRule.addTester(theTester);
}
if (myOpRule != null) {
myOpRule.addTester(theTester);
}
myOpRule.addTester(theTester);
return this;
}
@ -236,6 +209,12 @@ public class RuleBuilder implements IAuthRuleBuilder {
return new RuleBuilderRuleOperation();
}
@Override
public IAuthRuleBuilderPatch patch() {
myRuleOp = RuleOpEnum.PATCH;
return new PatchBuilder();
}
@Override
public IAuthRuleBuilderRuleOp read() {
myRuleOp = RuleOpEnum.READ;
@ -286,8 +265,8 @@ public class RuleBuilder implements IAuthRuleBuilder {
public class RuleBuilderRuleConditionalClassifier extends RuleBuilderFinished implements IAuthRuleBuilderRuleConditionalClassifier {
public RuleBuilderRuleConditionalClassifier() {
super((RuleImplOp) null);
RuleBuilderRuleConditionalClassifier() {
super(null);
}
@Override
@ -329,7 +308,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
Validate.notBlank(theId.getValue(), "theId.getValue() must not be null or empty");
Validate.notBlank(theId.getIdPart(), "theId must contain an ID part");
return new RuleBuilderRuleOpClassifier(Arrays.asList(theId)).finished();
return new RuleBuilderRuleOpClassifier(Collections.singletonList(theId)).finished();
}
@Override
@ -435,7 +414,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
private String myOperationName;
public RuleBuilderRuleOperationNamed(String theOperationName) {
RuleBuilderRuleOperationNamed(String theOperationName) {
if (theOperationName != null && !theOperationName.startsWith("$")) {
myOperationName = '$' + theOperationName;
} else {
@ -553,6 +532,17 @@ public class RuleBuilder implements IAuthRuleBuilder {
}
private class PatchBuilder implements IAuthRuleBuilderPatch {
@Override
public IAuthRuleFinished allRequests() {
BaseRule rule = new RuleImplPatch(myRuleName)
.setAllRequests(true)
.setMode(myRuleMode);
myRules.add(rule);
return new RuleBuilderFinished(rule);
}
}
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -33,7 +33,6 @@ public class RuleImplConditional extends BaseRule implements IAuthRule {
private AppliesTypeEnum myAppliesTo;
private Set<?> myAppliesToTypes;
private RestOperationTypeEnum myOperationType;
private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker;
RuleImplConditional(String theRuleName) {
super(theRuleName);
@ -43,6 +42,10 @@ public class RuleImplConditional extends BaseRule implements IAuthRule {
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource,
IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags) {
if (isOtherTenant(theRequestDetails)) {
return null;
}
if (theInputResourceId != null) {
return null;
}
@ -63,8 +66,8 @@ public class RuleImplConditional extends BaseRule implements IAuthRule {
return null;
}
if (myTenantApplicabilityChecker != null) {
if (!myTenantApplicabilityChecker.applies(theRequestDetails)) {
if (getTenantApplicabilityChecker() != null) {
if (!getTenantApplicabilityChecker().applies(theRequestDetails)) {
return null;
}
}
@ -91,8 +94,4 @@ public class RuleImplConditional extends BaseRule implements IAuthRule {
myOperationType = theOperationType;
}
public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
}
}

View File

@ -57,7 +57,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
private RuleOpEnum myOp;
private TransactionAppliesToEnum myTransactionAppliesToOp;
private List<IIdType> myAppliesToInstances;
private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker;
/**
* Constructor
@ -70,10 +69,8 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource,
IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags) {
if (myTenantApplicabilityChecker != null) {
if (!myTenantApplicabilityChecker.applies(theRequestDetails)) {
return null;
}
if (isOtherTenant(theRequestDetails)) {
return null;
}
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
@ -161,7 +158,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
case DELETE_TAGS:
case META_ADD:
case META_DELETE:
case PATCH:
appliesToResource = theInputResource;
appliesToResourceId = theInputResourceId;
break;
@ -454,6 +450,8 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
IBaseBundle request = (IBaseBundle) theInputResource;
String bundleType = BundleUtil.getBundleType(theContext, request);
//noinspection EnumSwitchStatementWhichMissesCases
switch (theOp) {
case TRANSACTION:
return "transaction".equals(bundleType);
@ -493,9 +491,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
return this;
}
public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
}
@Override
public String toString() {
@ -504,7 +499,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
builder.append("transactionAppliesToOp", myTransactionAppliesToOp);
builder.append("appliesTo", myAppliesTo);
builder.append("appliesToTypes", myAppliesToTypes);
builder.append("appliesToTenant", myTenantApplicabilityChecker);
builder.append("appliesToTenant", getTenantApplicabilityChecker());
builder.append("classifierCompartmentName", myClassifierCompartmentName);
builder.append("classifierCompartmentOwners", myClassifierCompartmentOwners);
builder.append("classifierType", myClassifierType);

View File

@ -0,0 +1,38 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Set;
class RuleImplPatch extends BaseRule {
private boolean myAllRequests;
RuleImplPatch(String theRuleName) {
super(theRuleName);
}
@Override
public AuthorizationInterceptor.Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags) {
if (isOtherTenant(theRequestDetails)) {
return null;
}
if (myAllRequests) {
if (theOperation == RestOperationTypeEnum.PATCH) {
if (theInputResource == null && theOutputResource == null) {
return newVerdict();
}
}
}
return null;
}
RuleImplPatch setAllRequests(boolean theAllRequests) {
myAllRequests = theAllRequests;
return this;
}
}

View File

@ -29,5 +29,6 @@ enum RuleOpEnum {
METADATA,
BATCH,
DELETE,
OPERATION
OPERATION,
PATCH
}

View File

@ -18,6 +18,7 @@ import ca.uhn.fhir.rest.server.interceptor.auth.*;
import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
@ -49,6 +50,9 @@ import java.util.concurrent.TimeUnit;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AuthorizationInterceptorR4Test {
@ -84,6 +88,16 @@ public class AuthorizationInterceptorR4Test {
return retVal;
}
private Resource createDiagnosticReport(Integer theId, String theSubjectId) {
DiagnosticReport retVal = new DiagnosticReport();
if (theId != null) {
retVal.setId(new IdType("DiagnosticReport", (long) theId));
}
retVal.getCode().setText("OBS");
retVal.setSubject(new Reference(theSubjectId));
return retVal;
}
private HttpEntity createFhirResourceEntity(IBaseResource theResource) {
String out = ourCtx.newJsonParser().encodeResourceToString(theResource);
return new StringEntity(out, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8"));
@ -99,16 +113,6 @@ public class AuthorizationInterceptorR4Test {
return retVal;
}
private Resource createDiagnosticReport(Integer theId, String theSubjectId) {
DiagnosticReport retVal = new DiagnosticReport();
if (theId != null) {
retVal.setId(new IdType("DiagnosticReport", (long) theId));
}
retVal.getCode().setText("OBS");
retVal.setSubject(new Reference(theSubjectId));
return retVal;
}
private Organization createOrganization(int theIndex) {
Organization retVal = new Organization();
retVal.setId("" + theIndex);
@ -1622,6 +1626,56 @@ public class AuthorizationInterceptorR4Test {
assertEquals(false, ourHitMethod);
}
@Test
public void testPatchAllowed() throws IOException {
Observation obs = new Observation();
obs.setSubject(new Reference("Patient/999"));
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().patch().allRequests().andThen()
.build();
}
});
String patchBody = "[\n" +
" { \"op\": \"replace\", \"path\": \"Observation/status\", \"value\": \"amended\" }\n" +
" ]";
HttpPatch patch = new HttpPatch("http://localhost:" + ourPort + "/Observation/123");
patch.setEntity(new StringEntity(patchBody, ContentType.create(Constants.CT_JSON_PATCH, Charsets.UTF_8)));
CloseableHttpResponse status = ourClient.execute(patch);
extractResponseAndClose(status);
assertEquals(204, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}
@Test
public void testPatchNotAllowed() throws IOException {
Observation obs = new Observation();
obs.setSubject(new Reference("Patient/999"));
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().metadata().andThen()
.build();
}
});
String patchBody = "[\n" +
" { \"op\": \"replace\", \"path\": \"Observation/status\", \"value\": \"amended\" }\n" +
" ]";
HttpPatch patch = new HttpPatch("http://localhost:" + ourPort + "/Observation/123");
patch.setEntity(new StringEntity(patchBody, ContentType.create(Constants.CT_JSON_PATCH, Charsets.UTF_8)));
CloseableHttpResponse status = ourClient.execute(patch);
extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
@Test
public void testReadByAnyId() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@ -1816,6 +1870,43 @@ public class AuthorizationInterceptorR4Test {
}
@Test
public void testReadByCompartmentReadByIdParam() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("Rule 1").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen()
.build();
}
});
HttpGet httpGet;
HttpResponse status;
ourReturn = Collections.singletonList(createPatient(1));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/1");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourReturn = Collections.singletonList(createPatient(1));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=1");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/2");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
@Test
public void testReadByCompartmentReadByPatientParam() throws Exception {
@ -1856,45 +1947,6 @@ public class AuthorizationInterceptorR4Test {
assertFalse(ourHitMethod);
}
@Test
public void testReadByCompartmentReadByIdParam() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("Rule 1").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen()
.build();
}
});
HttpGet httpGet;
HttpResponse status;
ourReturn = Collections.singletonList(createPatient(1));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/1");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourReturn = Collections.singletonList(createPatient(1));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=1");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/2");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
@Test
public void testReadByCompartmentRight() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@ -2856,6 +2908,7 @@ public class AuthorizationInterceptorR4Test {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("Rule 1").patch().allRequests().andThen()
.allow("Rule 1").write().instance("Patient/900").andThen()
.build();
}
@ -2873,14 +2926,6 @@ public class AuthorizationInterceptorR4Test {
extractResponseAndClose(status);
assertEquals(204, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
httpPost = new HttpPatch("http://localhost:" + ourPort + "/Patient/999");
httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json")));
status = ourClient.execute(httpPost);
extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
@AfterClass
@ -2989,11 +3034,12 @@ public class AuthorizationInterceptorR4Test {
public Class<? extends IBaseResource> getResourceType() {
return DiagnosticReport.class;
}
@Search()
public List<Resource> search(
@OptionalParam(name = "subject") ReferenceParam theSubject,
@OptionalParam(name = "patient") ReferenceParam thePatient
) {
) {
ourHitMethod = true;
return ourReturn;
}
@ -3041,6 +3087,12 @@ public class AuthorizationInterceptorR4Test {
return (Parameters) new Parameters().setId("1");
}
@Patch
public MethodOutcome patch(@IdParam IdType theId, PatchTypeEnum thePatchType, @ResourceParam String theBody) {
ourHitMethod = true;
return new MethodOutcome().setId(theId.withVersion("2"));
}
@Read(version = true)
public Observation read(@IdParam IdType theId) {
ourHitMethod = true;
@ -3183,7 +3235,7 @@ public class AuthorizationInterceptorR4Test {
}
@Search()
public List<Resource> search(@OptionalParam(name="_id") IdType theIdParam) {
public List<Resource> search(@OptionalParam(name = "_id") IdType theIdParam) {
ourHitMethod = true;
return ourReturn;
}

View File

@ -100,6 +100,12 @@
could cause an error about the incorrect FHIR version. Thanks to
Rob Hausam for the pull request!
</action>
<action type="add">
A new method has been added to AuthorizationInterceptor that can be used to
create rules allowing FHIR patch operations. See
<![CDATA[<a href="http://hapifhir.io/doc_rest_server_security.html#Authorizing_Patch_Operations">Authorizing Patch Operations</a>]]>
for more information.
</action>
</release>
<release version="3.4.0" date="2018-05-28">
<action type="add">

View File

@ -215,6 +215,29 @@
</subsection>
<subsection name="Authorizing Patch Operations">
<p>
The FHIR <a href="http://hl7.org/fhir/http.html#patch">patch</a> operation
presents a challenge for authorization, as the incoming request often contains
very little detail about what is being modified.
</p>
<p>
In order to properly enforce authorization on a server that
allows the patch operation, a rule may be added that allows all
patch requests, as shown below.
</p>
<p>
This should be combined with server support for
<a href="http://hapifhir.io/doc_rest_server_security.html#Authorizing_Sub-Operations">Authorizing Sub-Operations</a>
as shown above.
</p>
<macro name="snippet">
<param name="id" value="patchAll" />
<param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" />
</macro>
</subsection>
<subsection name="Authorizing Multitenant Servers">
<p>