AuthorizationInterceptor can now allow make read or write authorization decisions on a resource by instance ID

This commit is contained in:
James Agnew 2017-03-24 14:43:17 +08:00
parent 645cf3aca7
commit 44c0075409
7 changed files with 203 additions and 16 deletions

View File

@ -21,7 +21,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
*/
enum AppliesTypeEnum {
ALL_RESOURCES, TYPES
ALL_RESOURCES, TYPES, INSTANCES

View File

@ -10,7 +10,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* 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
* 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,
@ -24,14 +24,14 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IAuthRuleBuilderAppliesTo<T> {
/**
* Rule applies to resources of the given type
*/
T resourcesOfType(Class<? extends IBaseResource> theType);
/**
* Rule applies to all resources
*/
T allResources();
/**
* Rule applies to resources of the given type
*/
T resourcesOfType(Class<? extends IBaseResource> theType);
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import org.hl7.fhir.instance.model.api.IIdType;
/*
* #%L
* HAPI FHIR - Core Library
@ -21,5 +23,39 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
*/
public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo<IAuthRuleBuilderRuleOpClassifier> {
// nothing
/**
* Rule applies to the resource with the given ID (e.g. <code>Patient/123</code>)
* <p>
* See the following examples which show how theId is interpreted:
* </p>
* <ul>
* <li><b><code>http://example.com/Patient/123</code></b> - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)</li>
* <li><b><code>Patient/123</code></b> - Any Patient resource with the ID "123" will be matched</li>
* <li><b><code>123</code></b> - Any resource of any type with the ID "123" will be matched</li>
* </ul>
*
* @param theId The ID of the resource to apply (e.g. <code>Patient/123</code>)
* @throws IllegalArgumentException If theId does not contain an ID with at least an ID part
* @throws NullPointerException If theId is null
*/
IAuthRuleFinished instance(String theId);
/**
* Rule applies to the resource with the given ID (e.g. <code>Patient/123</code>)
* <p>
* See the following examples which show how theId is interpreted:
* </p>
* <ul>
* <li><b><code>http://example.com/Patient/123</code></b> - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)</li>
* <li><b><code>Patient/123</code></b> - Any Patient resource with the ID "123" will be matched</li>
* <li><b><code>123</code></b> - Any resource of any type with the ID "123" will be matched</li>
* </ul>
*
* @param theId The ID of the resource to apply (e.g. <code>Patient/123</code>)
* @throws IllegalArgumentException If theId does not contain an ID with at least an ID part
* @throws NullPointerException If theId is null
*/
IAuthRuleFinished instance(IIdType theId);
}

View File

@ -25,6 +25,7 @@ import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
public class RuleBuilder implements IAuthRuleBuilder {
@ -233,6 +234,22 @@ public class RuleBuilder implements IAuthRuleBuilder {
private ClassifierTypeEnum myClassifierType;
private String myInCompartmentName;
private Collection<? extends IIdType> myInCompartmentOwners;
private List<IIdType> myAppliesToInstances;
/**
* Constructor
*/
public RuleBuilderRuleOpClassifier() {
super();
}
/**
* Constructor
*/
public RuleBuilderRuleOpClassifier(List<IIdType> theAppliesToInstances) {
myAppliesToInstances = theAppliesToInstances;
myAppliesTo = AppliesTypeEnum.INSTANCES;
}
private IAuthRuleBuilderRuleOpClassifierFinished finished() {
@ -241,6 +258,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
rule.setOp(myRuleOp);
rule.setAppliesTo(myAppliesTo);
rule.setAppliesToTypes(myAppliesToTypes);
rule.setAppliesToInstances(myAppliesToInstances);
rule.setClassifierType(myClassifierType);
rule.setClassifierCompartmentName(myInCompartmentName);
rule.setClassifierCompartmentOwners(myInCompartmentOwners);
@ -285,6 +303,21 @@ public class RuleBuilder implements IAuthRuleBuilder {
}
@Override
public IAuthRuleFinished instance(String theId) {
Validate.notBlank(theId, "theId must not be null or empty");
return instance(new IdDt(theId));
}
@Override
public IAuthRuleFinished instance(IIdType theId) {
Validate.notNull(theId, "theId must not be null");
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();
}
}
private class RuleBuilderRuleOperation implements IAuthRuleBuilderOperation {

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
* #%L
* HAPI FHIR - Core Library
@ -41,7 +43,7 @@ import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.BundleUtil.BundleEntryParts;
import ca.uhn.fhir.util.FhirTerser;
class RuleImplOp extends BaseRule /*implements IAuthRule*/ {
class RuleImplOp extends BaseRule /* implements IAuthRule */ {
private AppliesTypeEnum myAppliesTo;
private Set<?> myAppliesToTypes;
@ -50,6 +52,7 @@ class RuleImplOp extends BaseRule /*implements IAuthRule*/ {
private ClassifierTypeEnum myClassifierType;
private RuleOpEnum myOp;
private TransactionAppliesToEnum myTransactionAppliesToOp;
private List<IIdType> myAppliesToInstances;
public RuleImplOp(String theRuleName) {
super(theRuleName);
@ -72,7 +75,7 @@ class RuleImplOp extends BaseRule /*implements IAuthRule*/ {
appliesToResourceId = theInputResourceId;
appliesToResourceType = theInputResourceId.getResourceType();
break;
// return new Verdict(PolicyEnum.ALLOW, this);
// return new Verdict(PolicyEnum.ALLOW, this);
case SEARCH_SYSTEM:
case SEARCH_TYPE:
case HISTORY_INSTANCE:
@ -195,6 +198,21 @@ class RuleImplOp extends BaseRule /*implements IAuthRule*/ {
}
switch (myAppliesTo) {
case INSTANCES:
if (appliesToResourceId != null) {
for (IIdType next : myAppliesToInstances) {
if (isNotBlank(next.getResourceType())) {
if (!next.getResourceType().equals(appliesToResourceId.getResourceType())) {
continue;
}
}
if (!next.getIdPart().equals(appliesToResourceId.getIdPart())) {
continue;
}
return newVerdict();
}
}
return null;
case ALL_RESOURCES:
if (appliesToResourceType != null) {
return new Verdict(PolicyEnum.ALLOW, this);
@ -317,4 +335,8 @@ class RuleImplOp extends BaseRule /*implements IAuthRule*/ {
myTransactionAppliesToOp = theOp;
}
public void setAppliesToInstances(List<IIdType> theAppliesToInstances) {
myAppliesToInstances = theAppliesToInstances;
}
}

View File

@ -2,10 +2,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -1559,7 +1556,7 @@ public class AuthorizationInterceptorDstu2Test {
httpPost.setEntity(createFhirResourceEntity(createObservation(10, "Patient/1")));
status = ourClient.execute(httpPost);
extractResponseAndClose(status);
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
@ -1684,6 +1681,102 @@ public class AuthorizationInterceptorDstu2Test {
}
@Test
public void testInvalidInstanceIds() throws Exception {
try {
new RuleBuilder().allow("Rule 1").write().instance((String)null);
fail();
} catch (NullPointerException e) {
assertEquals("theId must not be null or empty", e.getMessage());
}
try {
new RuleBuilder().allow("Rule 1").write().instance("");
fail();
} catch (IllegalArgumentException e) {
assertEquals("theId must not be null or empty", e.getMessage());
}
try {
new RuleBuilder().allow("Rule 1").write().instance("Observation/");
fail();
} catch (IllegalArgumentException e) {
assertEquals("theId must contain an ID part", e.getMessage());
}
try {
new RuleBuilder().allow("Rule 1").write().instance(new IdDt());
fail();
} catch (NullPointerException e) {
assertEquals("theId.getValue() must not be null or empty", e.getMessage());
}
try {
new RuleBuilder().allow("Rule 1").write().instance(new IdDt(""));
fail();
} catch (NullPointerException e) {
assertEquals("theId.getValue() must not be null or empty", e.getMessage());
}
try {
new RuleBuilder().allow("Rule 1").write().instance(new IdDt("Observation", (String)null));
fail();
} catch (NullPointerException e) {
assertEquals("theId must contain an ID part", e.getMessage());
}
}
@Test
public void testWriteByInstance() throws Exception {
ourConditionalCreateId = "1";
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
//@formatter:off
return new RuleBuilder()
.allow("Rule 1").write().instance("Observation/900").andThen()
.allow("Rule 1").write().instance("901").andThen()
.build();
//@formatter:on
}
});
HttpEntityEnclosingRequestBase httpPost;
HttpResponse status;
String response;
ourHitMethod = false;
httpPost = new HttpPut("http://localhost:" + ourPort + "/Observation/900");
httpPost.setEntity(createFhirResourceEntity(createObservation(900, "Patient/12")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
httpPost = new HttpPut("http://localhost:" + ourPort + "/Observation/901");
httpPost.setEntity(createFhirResourceEntity(createObservation(901, "Patient/12")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
httpPost = new HttpPost("http://localhost:" + ourPort + "/Observation");
httpPost.setEntity(createFhirResourceEntity(createObservation(null, "Patient/900")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertEquals(ERR403, response);
assertFalse(ourHitMethod);
ourHitMethod = false;
httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.setEntity(createFhirResourceEntity(createPatient(null)));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertEquals(ERR403, response);
assertFalse(ourHitMethod);
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
@ -1821,7 +1914,6 @@ public class AuthorizationInterceptorDstu2Test {
}
MethodOutcome retVal = new MethodOutcome();
retVal.setCreated(true);
retVal.setResource(theResource);
return retVal;
}

View File

@ -13,6 +13,10 @@
CORS headers. Thanks to GitHub user @elnin0815 for
the pull request!
</action>
<action type="add">
AuthorizationInterceptor can now allow make read or write
authorization decisions on a resource by instance ID
</action>
</release>
<release version="2.3" date="2017-03-18">
<action type="add">