Require explicit declaration of authorizationinterceptor operation rules

on whether the response is authorized or not
This commit is contained in:
James Agnew 2018-11-23 14:25:46 -05:00
parent 364b6cc5fd
commit b41c222880
10 changed files with 244 additions and 107 deletions

View File

@ -34,11 +34,11 @@ public class PublicSecurityInterceptor extends AuthorizationInterceptor {
if (isBlank(authHeader)) { if (isBlank(authHeader)) {
return new RuleBuilder() return new RuleBuilder()
.deny().operation().named(BaseJpaSystemProvider.MARK_ALL_RESOURCES_FOR_REINDEXING).onServer().andThen() .deny().operation().named(BaseJpaSystemProvider.MARK_ALL_RESOURCES_FOR_REINDEXING).onServer().andAllowAllResponses().andThen()
.deny().operation().named(BaseTerminologyUploaderProvider.UPLOAD_EXTERNAL_CODE_SYSTEM).onServer().andThen() .deny().operation().named(BaseTerminologyUploaderProvider.UPLOAD_EXTERNAL_CODE_SYSTEM).onServer().andAllowAllResponses().andThen()
.deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onServer().andThen() .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onServer().andAllowAllResponses().andThen()
.deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onAnyType().andThen() .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onAnyType().andAllowAllResponses().andThen()
.deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onAnyInstance().andThen() .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onAnyInstance().andAllowAllResponses().andThen()
.allowAll() .allowAll()
.build(); .build();
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -108,5 +108,4 @@ abstract class BaseRule implements IAuthRule {
Verdict newVerdict() { Verdict newVerdict() {
return new Verdict(myMode, this); return new Verdict(myMode, this);
} }
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -28,36 +28,36 @@ public interface IAuthRuleBuilderOperationNamed {
/** /**
* Rule applies to invocations of this operation at the <code>server</code> level * Rule applies to invocations of this operation at the <code>server</code> level
*/ */
IAuthRuleBuilderRuleOpClassifierFinished onServer(); IAuthRuleBuilderOperationNamedAndScoped onServer();
/** /**
* Rule applies to invocations of this operation at the <code>type</code> level * Rule applies to invocations of this operation at the <code>type</code> level
*/ */
IAuthRuleBuilderRuleOpClassifierFinished onType(Class<? extends IBaseResource> theType); IAuthRuleBuilderOperationNamedAndScoped onType(Class<? extends IBaseResource> theType);
/** /**
* Rule applies to invocations of this operation at the <code>type</code> level on any type * Rule applies to invocations of this operation at the <code>type</code> level on any type
*/ */
IAuthRuleBuilderRuleOpClassifierFinished onAnyType(); IAuthRuleBuilderOperationNamedAndScoped onAnyType();
/** /**
* Rule applies to invocations of this operation at the <code>instance</code> level * Rule applies to invocations of this operation at the <code>instance</code> level
*/ */
IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId); IAuthRuleBuilderOperationNamedAndScoped onInstance(IIdType theInstanceId);
/** /**
* Rule applies to invocations of this operation at the <code>instance</code> level on any instance of the given type * Rule applies to invocations of this operation at the <code>instance</code> level on any instance of the given type
*/ */
IAuthRuleBuilderRuleOpClassifierFinished onInstancesOfType(Class<? extends IBaseResource> theType); IAuthRuleBuilderOperationNamedAndScoped onInstancesOfType(Class<? extends IBaseResource> theType);
/** /**
* Rule applies to invocations of this operation at the <code>instance</code> level on any instance * Rule applies to invocations of this operation at the <code>instance</code> level on any instance
*/ */
IAuthRuleBuilderRuleOpClassifierFinished onAnyInstance(); IAuthRuleBuilderOperationNamedAndScoped onAnyInstance();
/** /**
* Rule applies to invocations of this operation at any level (server, type or instance) * Rule applies to invocations of this operation at any level (server, type or instance)
*/ */
IAuthRuleBuilderRuleOpClassifierFinished atAnyLevel(); IAuthRuleBuilderOperationNamedAndScoped atAnyLevel();
} }

View File

@ -0,0 +1,19 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
public interface IAuthRuleBuilderOperationNamedAndScoped {
/**
* Responses for this operation will not be checked
*/
IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponses();
/**
* Responses for this operation must be authorized by other rules. For example, if this
* rule is authorizing the Patient $everything operation, there must be a separate
* rule (or rules) that actually authorize the user to read the
* resources being returned
*/
IAuthRuleBuilderRuleOpClassifierFinished andRequireExplicitResponseAuthorization();
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -41,6 +41,7 @@ class OperationRule extends BaseRule implements IAuthRule {
private boolean myAppliesToAnyType; private boolean myAppliesToAnyType;
private boolean myAppliesToAnyInstance; private boolean myAppliesToAnyInstance;
private boolean myAppliesAtAnyLevel; private boolean myAppliesAtAnyLevel;
private boolean myAllowAllResponses;
OperationRule(String theRuleName) { OperationRule(String theRuleName) {
super(theRuleName); super(theRuleName);
@ -50,6 +51,10 @@ class OperationRule extends BaseRule implements IAuthRule {
myAppliesAtAnyLevel = theAppliesAtAnyLevel; myAppliesAtAnyLevel = theAppliesAtAnyLevel;
} }
public void allowAllResponses() {
myAllowAllResponses = true;
}
void appliesToAnyInstance() { void appliesToAnyInstance() {
myAppliesToAnyInstance = true; myAppliesToAnyInstance = true;
} }
@ -114,23 +119,32 @@ class OperationRule extends BaseRule implements IAuthRule {
case EXTENDED_OPERATION_INSTANCE: case EXTENDED_OPERATION_INSTANCE:
if (myAppliesToAnyInstance || myAppliesAtAnyLevel) { if (myAppliesToAnyInstance || myAppliesAtAnyLevel) {
applies = true; applies = true;
} else if (theInputResourceId != null) { } else {
if (myAppliesToIds != null) { IIdType requestResourceId = null;
String instanceId = theInputResourceId.toUnqualifiedVersionless().getValue(); if (theInputResourceId != null) {
for (IIdType next : myAppliesToIds) { requestResourceId = theInputResourceId;
if (next.toUnqualifiedVersionless().getValue().equals(instanceId)) { }
applies = true; if (requestResourceId == null && myAllowAllResponses) {
break; requestResourceId = theRequestDetails.getId();
}
if (requestResourceId != null) {
if (myAppliesToIds != null) {
String instanceId = requestResourceId .toUnqualifiedVersionless().getValue();
for (IIdType next : myAppliesToIds) {
if (next.toUnqualifiedVersionless().getValue().equals(instanceId)) {
applies = true;
break;
}
} }
} }
} if (myAppliesToInstancesOfType != null) {
if (myAppliesToInstancesOfType != null) { // TODO: Convert to a map of strings and keep the result
// TODO: Convert to a map of strings and keep the result for (Class<? extends IBaseResource> next : myAppliesToInstancesOfType) {
for (Class<? extends IBaseResource> next : myAppliesToInstancesOfType) { String resName = ctx.getResourceDefinition(next).getName();
String resName = ctx.getResourceDefinition(next).getName(); if (resName.equals(requestResourceId .getResourceType())) {
if (resName.equals(theInputResourceId.getResourceType())) { applies = true;
applies = true; break;
break; }
} }
} }
} }

View File

@ -416,6 +416,28 @@ public class RuleBuilder implements IAuthRuleBuilder {
private class RuleBuilderRuleOperationNamed implements IAuthRuleBuilderOperationNamed { private class RuleBuilderRuleOperationNamed implements IAuthRuleBuilderOperationNamed {
private class RuleBuilderOperationNamedAndScoped implements IAuthRuleBuilderOperationNamedAndScoped {
private final OperationRule myRule;
public RuleBuilderOperationNamedAndScoped(OperationRule theRule) {
myRule = theRule;
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponses() {
myRule.allowAllResponses();
myRules.add(myRule);
return new RuleBuilderFinished(myRule);
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinished andRequireExplicitResponseAuthorization() {
myRules.add(myRule);
return new RuleBuilderFinished(myRule);
}
}
private String myOperationName; private String myOperationName;
RuleBuilderRuleOperationNamed(String theOperationName) { RuleBuilderRuleOperationNamed(String theOperationName) {
@ -434,31 +456,28 @@ public class RuleBuilder implements IAuthRuleBuilder {
} }
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished onAnyInstance() { public IAuthRuleBuilderOperationNamedAndScoped onAnyInstance() {
OperationRule rule = createRule(); OperationRule rule = createRule();
rule.appliesToAnyInstance(); rule.appliesToAnyInstance();
myRules.add(rule); return new RuleBuilderOperationNamedAndScoped(rule);
return new RuleBuilderFinished(rule);
} }
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished atAnyLevel() { public IAuthRuleBuilderOperationNamedAndScoped atAnyLevel() {
OperationRule rule = createRule(); OperationRule rule = createRule();
rule.appliesAtAnyLevel(true); rule.appliesAtAnyLevel(true);
myRules.add(rule); return new RuleBuilderOperationNamedAndScoped(rule);
return new RuleBuilderFinished(rule);
} }
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished onAnyType() { public IAuthRuleBuilderOperationNamedAndScoped onAnyType() {
OperationRule rule = createRule(); OperationRule rule = createRule();
rule.appliesToAnyType(); rule.appliesToAnyType();
myRules.add(rule); return new RuleBuilderOperationNamedAndScoped(rule);
return new RuleBuilderFinished(rule);
} }
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId) { public IAuthRuleBuilderOperationNamedAndScoped onInstance(IIdType theInstanceId) {
Validate.notNull(theInstanceId, "theInstanceId must not be null"); Validate.notNull(theInstanceId, "theInstanceId must not be null");
Validate.notBlank(theInstanceId.getResourceType(), "theInstanceId does not have a resource type"); Validate.notBlank(theInstanceId.getResourceType(), "theInstanceId does not have a resource type");
Validate.notBlank(theInstanceId.getIdPart(), "theInstanceId does not have an ID part"); Validate.notBlank(theInstanceId.getIdPart(), "theInstanceId does not have an ID part");
@ -467,36 +486,32 @@ public class RuleBuilder implements IAuthRuleBuilder {
ArrayList<IIdType> ids = new ArrayList<>(); ArrayList<IIdType> ids = new ArrayList<>();
ids.add(theInstanceId); ids.add(theInstanceId);
rule.appliesToInstances(ids); rule.appliesToInstances(ids);
myRules.add(rule); return new RuleBuilderOperationNamedAndScoped(rule);
return new RuleBuilderFinished(rule);
} }
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished onInstancesOfType(Class<? extends IBaseResource> theType) { public IAuthRuleBuilderOperationNamedAndScoped onInstancesOfType(Class<? extends IBaseResource> theType) {
validateType(theType); validateType(theType);
OperationRule rule = createRule(); OperationRule rule = createRule();
rule.appliesToInstancesOfType(toTypeSet(theType)); rule.appliesToInstancesOfType(toTypeSet(theType));
myRules.add(rule); return new RuleBuilderOperationNamedAndScoped(rule);
return new RuleBuilderFinished(rule);
} }
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished onServer() { public IAuthRuleBuilderOperationNamedAndScoped onServer() {
OperationRule rule = createRule(); OperationRule rule = createRule();
rule.appliesToServer(); rule.appliesToServer();
myRules.add(rule); return new RuleBuilderOperationNamedAndScoped(rule);
return new RuleBuilderFinished(rule);
} }
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished onType(Class<? extends IBaseResource> theType) { public IAuthRuleBuilderOperationNamedAndScoped onType(Class<? extends IBaseResource> theType) {
validateType(theType); validateType(theType);
OperationRule rule = createRule(); OperationRule rule = createRule();
rule.appliesToTypes(toTypeSet(theType)); rule.appliesToTypes(toTypeSet(theType));
myRules.add(rule); return new RuleBuilderOperationNamedAndScoped(rule);
return new RuleBuilderFinished(rule);
} }
private HashSet<Class<? extends IBaseResource>> toTypeSet(Class<? extends IBaseResource> theType) { private HashSet<Class<? extends IBaseResource>> toTypeSet(Class<? extends IBaseResource> theType) {

View File

@ -572,7 +572,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().withAnyName().onServer().andThen() .allow("RULE 1").operation().withAnyName().onServer().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -598,7 +598,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization()
.build(); .build();
} }
}); });
@ -633,7 +633,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andThen() .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().andThen()
.allow("Rule 2").read().allResources().inCompartment("Patient", new IdDt("Patient/1")).andThen() .allow("Rule 2").read().allResources().inCompartment("Patient", new IdDt("Patient/1")).andThen()
.build(); .build();
} }
@ -671,7 +671,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization()
.build(); .build();
} }
}); });
@ -705,7 +705,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onInstance(new IdDt("http://example.com/Patient/1/_history/2")).andThen() .allow("RULE 1").operation().named("opName").onInstance(new IdDt("http://example.com/Patient/1/_history/2")).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -764,7 +764,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onAnyInstance().andThen() .allow("RULE 1").operation().named("opName").onAnyInstance().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -890,7 +890,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onServer().andThen() .allow("RULE 1").operation().named("opName").onServer().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -937,7 +937,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1006,7 +1006,7 @@ public class AuthorizationInterceptorDstu2Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onAnyType().andThen() .allow("RULE 1").operation().named("opName").onAnyType().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });

View File

@ -882,7 +882,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().withAnyName().onServer().andThen() .allow("RULE 1").operation().withAnyName().onServer().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -908,7 +908,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").atAnyLevel().andThen() .allow("RULE 1").operation().named("opName").atAnyLevel().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -964,7 +964,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opNameBadOp").atAnyLevel().andThen() .allow("RULE 1").operation().named("opNameBadOp").atAnyLevel().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1020,7 +1020,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization()
.build(); .build();
} }
}); });
@ -1055,7 +1055,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andThen() .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().andThen()
.allow("Rule 2").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen() .allow("Rule 2").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen()
.build(); .build();
} }
@ -1093,7 +1093,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization()
.build(); .build();
} }
}); });
@ -1127,7 +1127,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onInstance(new IdType("http://example.com/Patient/1/_history/2")).andThen() .allow("RULE 1").operation().named("opName").onInstance(new IdType("http://example.com/Patient/1/_history/2")).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1186,7 +1186,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onAnyInstance().andThen() .allow("RULE 1").operation().named("opName").onAnyInstance().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1311,7 +1311,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onServer().andThen() .allow("RULE 1").operation().named("opName").onServer().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1358,7 +1358,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1427,7 +1427,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onAnyType().andThen() .allow("RULE 1").operation().named("opName").onAnyType().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1495,7 +1495,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onType(Organization.class).andThen() .allow("RULE 1").operation().named("opName").onType(Organization.class).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1554,7 +1554,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onType(Patient.class).forTenantIds("TENANTA").andThen() .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().forTenantIds("TENANTA").andThen()
.build(); .build();
} }
}); });
@ -1591,7 +1591,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andThen() .allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1630,7 +1630,7 @@ public class AuthorizationInterceptorDstu3Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).withTester(new IAuthRuleTester() { .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().withTester(new IAuthRuleTester() {
@Override @Override
public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) { public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) {
return theInputResourceId.getIdPart().equals("1"); return theInputResourceId.getIdPart().equals("1");

View File

@ -36,11 +36,10 @@ import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.*;
import org.junit.AfterClass; import org.junit.*;
import org.junit.Before; import org.springframework.util.Base64Utils;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
@ -49,6 +48,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static javax.print.DocFlavor.READER.TEXT_HTML;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -65,6 +65,7 @@ public class AuthorizationInterceptorR4Test {
private static List<Resource> ourReturn; private static List<Resource> ourReturn;
private static Server ourServer; private static Server ourServer;
private static RestfulServer ourServlet; private static RestfulServer ourServlet;
private static String ourLastAcceptHeader;
@Before @Before
public void before() { public void before() {
@ -76,6 +77,7 @@ public class AuthorizationInterceptorR4Test {
ourReturn = null; ourReturn = null;
ourHitMethod = false; ourHitMethod = false;
ourConditionalCreateId = "1123"; ourConditionalCreateId = "1123";
ourLastAcceptHeader = null;
} }
private Resource createCarePlan(Integer theId, String theSubjectId) { private Resource createCarePlan(Integer theId, String theSubjectId) {
@ -922,7 +924,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().withAnyName().onServer().andThen() .allow("RULE 1").operation().withAnyName().onServer().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -948,7 +950,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").atAnyLevel().andThen() .allow("RULE 1").operation().named("opName").atAnyLevel().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1004,7 +1006,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opNameBadOp").atAnyLevel().andThen() .allow("RULE 1").operation().named("opNameBadOp").atAnyLevel().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1060,7 +1062,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization()
.build(); .build();
} }
}); });
@ -1090,12 +1092,13 @@ public class AuthorizationInterceptorR4Test {
} }
@Test @Test
@Ignore
public void testOperationByInstanceOfTypeWithInvalidReturnValue() throws Exception { public void testOperationByInstanceOfTypeWithInvalidReturnValue() 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) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andThen() .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().andThen()
.allow("Rule 2").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen() .allow("Rule 2").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen()
.build(); .build();
} }
@ -1133,7 +1136,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization()
.build(); .build();
} }
}); });
@ -1167,7 +1170,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onInstance(new IdType("http://example.com/Patient/1/_history/2")).andThen() .allow("RULE 1").operation().named("opName").onInstance(new IdType("http://example.com/Patient/1/_history/2")).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1226,7 +1229,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onAnyInstance().andThen() .allow("RULE 1").operation().named("opName").onAnyInstance().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1289,6 +1292,31 @@ public class AuthorizationInterceptorR4Test {
} }
@Test
public void testOperationInstanceLevelWithHtmlResponse() throws IOException {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("RULE 1").operation().named("binaryop").onInstancesOfType(Patient.class).andAllowAllResponses().andThen()
.build();
}
});
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2/$binaryop");
httpGet.addHeader(Constants.HEADER_ACCEPT, "text/html");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("text/html", status.getEntity().getContentType().getValue());
assertEquals("<html>TAGS</html>", IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8));
assertEquals("text/html", ourLastAcceptHeader);
}
}
@Test @Test
public void testOperationNotAllowedWithWritePermissiom() throws Exception { public void testOperationNotAllowedWithWritePermissiom() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@ -1346,12 +1374,12 @@ public class AuthorizationInterceptorR4Test {
} }
@Test @Test
public void testOperationServerLevel() throws Exception { public void testOperationServerLevelAllowAllResponses() 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) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onServer().andThen() .allow("RULE 1").operation().named("opName").onServer().andAllowAllResponses().andThen()
.build(); .build();
} }
}); });
@ -1392,13 +1420,48 @@ public class AuthorizationInterceptorR4Test {
assertFalse(ourHitMethod); assertFalse(ourHitMethod);
} }
@Test
public void testOperationServerLevelRequireResponseAuthorization() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onServer().andRequireExplicitResponseAuthorization().andThen()
.allow().read().instance("Observation/10").andThen()
.build();
}
});
HttpGet httpGet;
HttpResponse status;
String response;
// Server
ourHitMethod = false;
ourReturn = Collections.singletonList(createObservation(10, "Patient/2"));
httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
// Server
ourHitMethod = false;
ourReturn = Collections.singletonList(createObservation(99, "Patient/2"));
httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}
@Test @Test
public void testOperationTypeLevel() throws Exception { public void testOperationTypeLevel() 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) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1467,7 +1530,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onAnyType().andThen() .allow("RULE 1").operation().named("opName").onAnyType().andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1535,7 +1598,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onType(Organization.class).andThen() .allow("RULE 1").operation().named("opName").onType(Organization.class).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1594,7 +1657,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onType(Patient.class).forTenantIds("TENANTA").andThen() .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().forTenantIds("TENANTA").andThen()
.build(); .build();
} }
}); });
@ -1631,7 +1694,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andThen() .allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andRequireExplicitResponseAuthorization().andThen()
.build(); .build();
} }
}); });
@ -1670,7 +1733,7 @@ public class AuthorizationInterceptorR4Test {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).withTester(new IAuthRuleTester() { .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().withTester(new IAuthRuleTester() {
@Override @Override
public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) { public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) {
return theInputResourceId.getIdPart().equals("1"); return theInputResourceId.getIdPart().equals("1");
@ -3264,6 +3327,7 @@ public class AuthorizationInterceptorR4Test {
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static class DummyPatientResourceProvider implements IResourceProvider { public static class DummyPatientResourceProvider implements IResourceProvider {
@Create() @Create()
public MethodOutcome create(@ResourceParam Patient theResource, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails) { public MethodOutcome create(@ResourceParam Patient theResource, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails) {
@ -3303,6 +3367,26 @@ public class AuthorizationInterceptorR4Test {
return retVal; return retVal;
} }
@Operation(name = "$binaryop", idempotent = true)
public Binary binaryOp(
@IdParam IIdType theId,
@OperationParam(name = "PARAM3", min = 0, max = 1) List<org.hl7.fhir.r4.model.StringType> theParam3,
HttpServletRequest theServletRequest
) {
ourLastAcceptHeader = theServletRequest.getHeader(ca.uhn.fhir.rest.api.Constants.HEADER_ACCEPT);
Binary retVal = new Binary();
if (ourLastAcceptHeader.contains("html")) {
retVal.setContentType("text/html");
retVal.setContent("<html>TAGS</html>".getBytes(Charsets.UTF_8));
} else {
retVal.setContentType("application/weird");
retVal.setContent(new byte[]{0,0,1,1,2,2,3,3,0,0});
}
return retVal;
}
@Override @Override
public Class<? extends IBaseResource> getResourceType() { public Class<? extends IBaseResource> getResourceType() {
return Patient.class; return Patient.class;

View File

@ -19,14 +19,14 @@
<action type="add"> <action type="add">
Changed subscription processing, if the subscription criteria are straightforward (i.e. no Changed subscription processing, if the subscription criteria are straightforward (i.e. no
chained references, qualifiers or prefixes) then attempt to match the incoming resource against chained references, qualifiers or prefixes) then attempt to match the incoming resource against
the criteria in-memory. If the subscription criteria can't be matched in-memory, then the the criteria in-memory. If the subscription criteria can't be matched in-memory, then the
server falls back to the original subscription matching process of querying the database. The server falls back to the original subscription matching process of querying the database. The
in-memory matcher can be disabled by setting isEnableInMemorySubscriptionMatching to "false" in in-memory matcher can be disabled by setting isEnableInMemorySubscriptionMatching to "false" in
DaoConfig (by default it is true). If isEnableInMemorySubscriptionMatching is "false", then all DaoConfig (by default it is true). If isEnableInMemorySubscriptionMatching is "false", then all
subscription matching will query the database as before. subscription matching will query the database as before.
</action> </action>
<action type="add"> <action type="add">
Changed behaviour of FHIR Server to reject subscriptions with invalid criteria. If a Subscription Changed behaviour of FHIR Server to reject subscriptions with invalid criteria. If a Subscription
is submitted with invalid criteria, the server returns HTTP 422 "Unprocessable Entity" and the is submitted with invalid criteria, the server returns HTTP 422 "Unprocessable Entity" and the
Subscription is not persisted. Subscription is not persisted.
</action> </action>
@ -76,14 +76,15 @@
ResourceIndexedSearchParams, IdHelperService, SearcchParamExtractorService, and MatchUrlService. ResourceIndexedSearchParams, IdHelperService, SearcchParamExtractorService, and MatchUrlService.
</action> </action>
<action type="add"> <action type="add">
Replaced explicit @Bean construction in BaseConfig.java with @ComponentScan. Beans with state are annotated with Replaced explicit @Bean construction in BaseConfig.java with @ComponentScan. Beans with state are annotated
@Component and stateless beans are annotated as @Service. Also changed SearchBuilder.java and the with
@Component and stateless beans are annotated as @Service. Also changed SearchBuilder.java and the
three Subscriber classes into @Scope("protoype") so their dependencies can be @Autowired injected three Subscriber classes into @Scope("protoype") so their dependencies can be @Autowired injected
as opposed to constructor parameters. as opposed to constructor parameters.
</action> </action>
<action type="fix"> <action type="fix">
A bug in the JPA resource reindexer was fixed: In many cases the reindexer would A bug in the JPA resource reindexer was fixed: In many cases the reindexer would
mark reindexing jobs as deleted before they had actually completed, leading to mark reindexing jobs as deleted before they had actually completed, leading to
some resources not actually being reindexed. some resources not actually being reindexed.
</action> </action>
<action type="change"> <action type="change">
@ -91,6 +92,11 @@
larger batches (20000 instead of 500) in order to reduce the amount of noise larger batches (20000 instead of 500) in order to reduce the amount of noise
in the logs. in the logs.
</action> </action>
<action type="add">
AuthorizationInterceptor now allows arbitrary FHIR $operations to be authorized,
including support for either allowing the operation response to proceed unchallenged,
or authorizing the contents of the response.
</action>
</release> </release>
<release version="3.6.0" date="2018-11-12" description="Food"> <release version="3.6.0" date="2018-11-12" description="Food">
<action type="add"> <action type="add">