Change the IAuthRuleTester api to a parameter object. (#3756)

* Try using tester to implement fhir filtering.
This commit is contained in:
michaelabuckley 2022-07-05 22:08:45 -04:00 committed by GitHub
parent b68ccb373a
commit 616a126fad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 297 additions and 75 deletions

View File

@ -0,0 +1,4 @@
---
type: change
issue: 3756
title: "The IAuthRuleTester api has changed and now uses a parameter object for flexibility."

View File

@ -23,8 +23,14 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthorizationSearchParamMatcher;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Adapter from {@link SearchParamMatcher} to our authorization version.
*/
public class AuthorizationSearchParamMatcher implements IAuthorizationSearchParamMatcher {
private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationSearchParamMatcher.class);
private final SearchParamMatcher mySearchParamMatcher;
public AuthorizationSearchParamMatcher(SearchParamMatcher mySearchParamMatcher) {
@ -36,18 +42,20 @@ public class AuthorizationSearchParamMatcher implements IAuthorizationSearchPara
try {
InMemoryMatchResult inMemoryMatchResult = mySearchParamMatcher.match(theCriteria, theResource, null);
if (!inMemoryMatchResult.supported()) {
return new MatchResult(Match.UNSUPPORTED, inMemoryMatchResult.getUnsupportedReason());
return MatchResult.buildUnsupported(inMemoryMatchResult.getUnsupportedReason());
}
if (inMemoryMatchResult.matched()) {
return new MatchResult(Match.MATCH, null);
return MatchResult.buildMatched();
} else {
return new MatchResult(Match.NO_MATCH, null);
return MatchResult.buildUnmatched();
}
} catch (MatchUrlService.UnrecognizedSearchParameterException e) {
// wipmb revisit this design
// The matcher treats a bad expression as InvalidRequestException because it assumes it is during SearchParameter storage.
// Instead, we adapt this to UNSUPPORTED during authorization. We may be applying to all types, and this filter won't match.
return new MatchResult(Match.UNSUPPORTED, e.getMessage());
// The matcher treats a bad expression as InvalidRequestException because
// it assumes it is during SearchParameter storage.
// Instead, we adapt this to UNSUPPORTED during authorization.
// We may be applying to all types, and this filter won't match.
ourLog.info("Unsupported filter {} applied to resource: {}", theCriteria, e.getMessage());
return MatchResult.buildUnsupported(e.getMessage());
}
}
}

View File

@ -94,7 +94,7 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
.andThen().build().get(0);
when(myMatcher.match(ArgumentMatchers.eq("Patient?family=Smith"), ArgumentMatchers.same(myResource)))
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.makeMatched());
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildMatched());
// when
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);
@ -112,7 +112,7 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
.inCompartmentWithFilter("patient", myResource.getIdElement().withResourceType("Patient"), "family=smi")
.andThen().build().get(0);
when(myMatcher.match(ArgumentMatchers.eq("Patient?family=smi"), ArgumentMatchers.same(myResource)))
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.makeUnmatched());
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildUnmatched());
// when
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);
@ -133,7 +133,7 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
.inCompartmentWithFilter("patient", myResource.getIdElement().withResourceType("Patient"), "code=28521000087105")
.andThen().build().get(0);
when(myMatcher.match("Observation?code=28521000087105", myResource2))
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.makeUnmatched());
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildUnmatched());
// when
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource2);
@ -152,7 +152,7 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
.withFilter("code=12")
.andThen().build().get(0);
when(myMatcher.match("Observation?code=12", myResource2))
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.makeUnmatched());
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildUnmatched());
// when
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource2);
@ -171,7 +171,7 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
.withFilter("code=28521000087105")
.andThen().build().get(0);
when(myMatcher.match("Observation?code=28521000087105", myResource2))
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.makeMatched());
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildMatched());
// when
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource2);
@ -200,7 +200,7 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
myRule = (FhirQueryRuleImpl) new RuleBuilder().allow().read().resourcesOfType("Patient")
.inCompartmentWithFilter("patient", myResource.getIdElement().withResourceType("Patient"), "family=smi").andThen().build().get(0);
when(myMatcher.match("Patient?family=smi", myResource))
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.makeUnsupported("I'm broken unsupported chain XXX"));
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildUnsupported("I'm broken unsupported chain XXX"));
// when
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);
@ -217,7 +217,7 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
myRule = (FhirQueryRuleImpl) new RuleBuilder().deny().read().resourcesOfType("Patient")
.inCompartmentWithFilter("patient", myResource.getIdElement().withResourceType("Patient"), "family=smi").andThen().build().get(0);
when(myMatcher.match("Patient?family=smi", myResource))
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.makeUnsupported("I'm broken unsupported chain XXX"));
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildUnsupported("I'm broken unsupported chain XXX"));
// when
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);

View File

@ -29,7 +29,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -54,20 +53,24 @@ abstract class BaseRule implements IAuthRule {
theTesters.forEach(this::addTester);
}
private boolean applyTesters(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource, IBaseResource theOutputResource) {
private boolean applyTesters(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource, IBaseResource theOutputResource, IRuleApplier theRuleApplier) {
assert !(theInputResource != null && theOutputResource != null);
boolean retVal = true;
if (theOutputResource == null) {
IAuthRuleTester.RuleTestRequest inputRequest = new IAuthRuleTester.RuleTestRequest(myMode, theOperation, theRequestDetails, theInputResourceId, theInputResource, theRuleApplier);
for (IAuthRuleTester next : getTesters()) {
if (!next.matches(theOperation, theRequestDetails, theInputResourceId, theInputResource)) {
if (!next.matches(inputRequest)) {
retVal = false;
break;
}
}
} else {
IAuthRuleTester.RuleTestRequest outputRequest = new IAuthRuleTester.RuleTestRequest(myMode, theOperation, theRequestDetails, theOutputResource.getIdElement(), theOutputResource, theRuleApplier);
for (IAuthRuleTester next : getTesters()) {
if (!next.matchesOutput(theOperation, theRequestDetails, theOutputResource)) {
if (!next.matchesOutput(outputRequest)) {
retVal = false;
break;
}
@ -98,8 +101,8 @@ abstract class BaseRule implements IAuthRule {
return Collections.unmodifiableList(myTesters);
}
Verdict newVerdict(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) {
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
Verdict newVerdict(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier) {
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource, theRuleApplier)) {
return null;
}
return new Verdict(myMode, this);

View File

@ -0,0 +1,42 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
public class FhirQueryRuleTester implements IAuthRuleTester {
private final String myType;
private final String myFilter;
public FhirQueryRuleTester(String theType, String theFilter) {
myType = theType;
myFilter = theFilter;
}
@Override
public boolean matches(RuleTestRequest theRuleTestRequest) {
// wipmb placeholder until we get to writes
return true;
}
@Override
public boolean matchesOutput(RuleTestRequest theRuleTestRequest) {
// look for a matcher
IAuthorizationSearchParamMatcher matcher = theRuleTestRequest.ruleApplier.getSearchParamMatcher();
if (matcher == null) {
return false;
}
// wipmb myType, or target type? Is this just query filter, or do we bring * into it?
IAuthorizationSearchParamMatcher.MatchResult mr = matcher.match(myType + "?" + myFilter, theRuleTestRequest.resource);
switch (mr.getMatch()) {
case MATCH:
return true;
case UNSUPPORTED:
theRuleTestRequest.ruleApplier.getTroubleshootingLog().warn("Unsupported matcher expression {}: {}.", myFilter, mr.getUnsupportedReason());
// unsupported doesn't match unless this is a deny request, and we need to be safe!
return (theRuleTestRequest.mode == PolicyEnum.DENY);
case NO_MATCH:
default:
return false;
}
}
}

View File

@ -22,9 +22,13 @@ 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.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Allows user-supplied logic for authorization rules.
* <p>
@ -34,6 +38,44 @@ import org.hl7.fhir.instance.model.api.IIdType;
* @since 3.4.0
*/
public interface IAuthRuleTester {
/**
* A request object to make this easier to extend.
*/
class RuleTestRequest {
// fake record pattern
/** the mode of the calling rule context */
@Nonnull public final PolicyEnum mode;
/**
* The FHIR operation being performed.
* Note that this is not necessarily the same as the value obtained from invoking
* {@link RequestDetails#getRestOperationType()} on {@literal requestDetails}
* because multiple operations can be nested within
* an HTTP request using FHIR transaction and batch operations
*/
@Nonnull public final RestOperationTypeEnum operation;
@Nonnull public final RequestDetails requestDetails;
@Nullable public final IIdType resourceId;
@Nullable public final IBaseResource resource;
/** supplier for support services */
@Nonnull public final IRuleApplier ruleApplier;
public RuleTestRequest(PolicyEnum theMode, @Nonnull RestOperationTypeEnum theOperation, @Nonnull RequestDetails theRequestDetails, @Nullable IIdType theResourceId, @Nullable IBaseResource theResource, @Nonnull IRuleApplier theRuleApplier) {
Validate.notNull(theMode);
Validate.notNull(theOperation);
Validate.notNull(theRequestDetails);
Validate.notNull(theRuleApplier);
mode = theMode;
operation = theOperation;
requestDetails = theRequestDetails;
resource = theResource;
if (theResourceId == null && resource != null) {
resourceId = resource.getIdElement();
} else {
resourceId = theResourceId;
}
ruleApplier = theRuleApplier;
}
}
/**
* Allows user-supplied logic for authorization rules.
@ -41,11 +83,19 @@ public interface IAuthRuleTester {
* THIS IS AN EXPERIMENTAL API! Feedback is welcome, and this API
* may change.
*
* @param theOperation The FHIR operation being performed - Note that this is not necessarily the same as the value obtained from invoking
* {@link RequestDetails#getRestOperationType()} on {@literal theRequestDetails} because multiple operations can be nested within
* an HTTP request using FHIR transaction and batch operations
* @since 3.4.0
* @param theRequest The details to evaluate
* @since 6.1.0
*/
default boolean matches(RuleTestRequest theRequest) {
return this.matches(theRequest.operation, theRequest.requestDetails, theRequest.resourceId, theRequest.resource);
}
/**
* DO NOT IMPLEMENT - Old api. {@link #matches(RuleTestRequest)} instead.
* @deprecated
*/
@Deprecated(since = "6.1.0")
default boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) {
return true;
}
@ -56,11 +106,18 @@ public interface IAuthRuleTester {
* THIS IS AN EXPERIMENTAL API! Feedback is welcome, and this API
* may change.
*
* @param theOperation The FHIR operation being performed - Note that this is not necessarily the same as the value obtained from invoking
* {@link RequestDetails#getRestOperationType()} on {@literal theRequestDetails} because multiple operations can be nested within
* an HTTP request using FHIR transaction and batch operations
* @since 5.0.0
* @param theRequest The details to evaluate
* @since 6.1.0
*/
default boolean matchesOutput(RuleTestRequest theRequest) {
return this.matchesOutput(theRequest.operation, theRequest.requestDetails, theRequest.resource);
}
/**
* DO NOT IMPLEMENT - Old api. {@link #matches(RuleTestRequest)} instead.
* @deprecated
*/
@Deprecated(since = "6.1.0")
default boolean matchesOutput(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theOutputResource) {
return true;
}

View File

@ -23,36 +23,42 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* wipjv Ken do we like this name, or this package?
* Adapt the InMemoryMatcher to support authorization filters in {@link FhirQueryRuleImpl}.
*/
public interface IAuthorizationSearchParamMatcher {
MatchResult match(String theCriteria, IBaseResource theResource);
/**
* Match outcomes.
*/
enum Match {
MATCH,
NO_MATCH,
/** Used for contexts without matcher infrastructure like hybrid providers */
UNSUPPORTED
}
public static class MatchResult {
private final Match myMatch;
private final String myUnsupportedReason;
class MatchResult {
// wipmb consider a record pattern - public and drop the accessors.
@Nonnull private final Match myMatch;
@Nullable private final String myUnsupportedReason;
public static MatchResult makeMatched() {
public static MatchResult buildMatched() {
return new MatchResult(Match.MATCH, null);
}
public static MatchResult makeUnmatched() {
public static MatchResult buildUnmatched() {
return new MatchResult(Match.NO_MATCH, null);
}
public static MatchResult makeUnsupported(@Nonnull String theReason) {
public static MatchResult buildUnsupported(@Nonnull String theReason) {
return new MatchResult(Match.UNSUPPORTED, theReason);
}
public MatchResult(Match myMatch, String myUnsupportedReason) {
private MatchResult(Match myMatch, String myUnsupportedReason) {
this.myMatch = myMatch;
this.myUnsupportedReason = myUnsupportedReason;
}

View File

@ -199,7 +199,7 @@ class OperationRule extends BaseRule implements IAuthRule {
return null;
}
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
/**

View File

@ -583,7 +583,6 @@ public class RuleBuilder implements IAuthRuleBuilder {
@Override
public IAuthRuleFinished inCompartmentWithFilter(String theCompartmentName, IIdType theIdElement, String theFilter) {
// wipjv (resolved?) implemented
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null");
Validate.notNull(theIdElement, "theOwner must not be null");
validateOwner(theIdElement);

View File

@ -72,14 +72,14 @@ public class RuleBulkExportImpl extends BaseRule {
}
if (myWantAnyStyle || myWantExportStyle == BulkDataExportOptions.ExportStyle.SYSTEM) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
if (isNotBlank(myGroupId) && options.getGroupId() != null) {
String expectedGroupId = new IdDt(myGroupId).toUnqualifiedVersionless().getValue();
String actualGroupId = options.getGroupId().toUnqualifiedVersionless().getValue();
if (Objects.equals(expectedGroupId, actualGroupId)) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
}
return null;

View File

@ -72,7 +72,7 @@ public class RuleImplConditional extends BaseRule implements IAuthRule {
break;
}
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
return null;

View File

@ -115,12 +115,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
case SEARCH_SYSTEM:
case HISTORY_SYSTEM:
if (theFlags.contains(AuthorizationFlagsEnum.DO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
break;
case SEARCH_TYPE:
if (theFlags.contains(AuthorizationFlagsEnum.DO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
target.resourceType = theRequestDetails.getResourceName();
target.setSearchParams(theRequestDetails);
@ -135,18 +135,18 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
break;
case HISTORY_TYPE:
if (theFlags.contains(AuthorizationFlagsEnum.DO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
target.resourceType = theRequestDetails.getResourceName();
break;
case HISTORY_INSTANCE:
if (theFlags.contains(AuthorizationFlagsEnum.DO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
target.resourceIds = Collections.singleton(theInputResourceId);
break;
case GET_PAGE:
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
// None of the following are checked on the way in
case ADD_TAGS:
@ -222,7 +222,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
case DELETE:
if (theOperation == RestOperationTypeEnum.DELETE) {
if (thePointcut == Pointcut.STORAGE_PRE_DELETE_EXPUNGE && myAppliesToDeleteExpunge) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
if (myAppliesToDeleteCascade != (thePointcut == Pointcut.STORAGE_CASCADE_DELETE)) {
return null;
@ -233,10 +233,10 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
if (theInputResourceId.hasIdPart() == false) {
// This is a conditional DELETE, so we'll authorize it using STORAGE events instead
// so just let it through for now..
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
if (theInputResource == null && myClassifierCompartmentOwners != null && myClassifierCompartmentOwners.size() > 0) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
target.resource = theInputResource;
@ -246,14 +246,14 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
}
break;
case GRAPHQL:
return applyRuleToGraphQl(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, thePointcut);
return applyRuleToGraphQl(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, thePointcut, theRuleApplier);
case TRANSACTION:
return applyRuleToTransaction(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier, thePointcut, ctx);
case ALL:
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
case METADATA:
if (theOperation == RestOperationTypeEnum.METADATA) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
return null;
default:
@ -263,11 +263,11 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
switch (myAppliesTo) {
case INSTANCES:
return applyRuleToInstances(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, target);
return applyRuleToInstances(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, target, theRuleApplier);
case ALL_RESOURCES:
if (target.resourceType != null) {
if (myClassifierType == ClassifierTypeEnum.ANY_ID) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
}
break;
@ -295,7 +295,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
return null;
}
if (myClassifierType == ClassifierTypeEnum.ANY_ID) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
} else if (myClassifierType == ClassifierTypeEnum.IN_COMPARTMENT) {
// ok we'll check below
}
@ -320,16 +320,16 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
case ANY_ID:
break;
case IN_COMPARTMENT:
return applyRuleToCompartment(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theFlags, theFhirContext, theRuleTarget);
return applyRuleToCompartment(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theFlags, theFhirContext, theRuleTarget, theRuleApplier);
default:
throw new IllegalStateException(Msg.code(337) + "Unable to apply security to event of applies to type " + myAppliesTo);
}
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
@Nullable
private Verdict applyRuleToGraphQl(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Pointcut thePointcut) {
private Verdict applyRuleToGraphQl(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Pointcut thePointcut, IRuleApplier theRuleApplier) {
if (theOperation == RestOperationTypeEnum.GRAPHQL_REQUEST) {
// Make sure that the requestor actually has sufficient access to see the given resource
@ -337,14 +337,14 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
return null;
}
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
} else {
return null;
}
}
@Nullable
private Verdict applyRuleToCompartment(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Set<AuthorizationFlagsEnum> theFlags, FhirContext ctx, RuleTarget target) {
private Verdict applyRuleToCompartment(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Set<AuthorizationFlagsEnum> theFlags, FhirContext ctx, RuleTarget target, IRuleApplier theRuleApplier) {
FhirTerser t = ctx.newTerser();
boolean foundMatch = false;
@ -377,7 +377,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
* it makes sense.
*/
if (next.getResourceType().equals(target.resourceType)) {
Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(target.getSearchParams(), next, SP_RES_ID, theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(target.getSearchParams(), next, SP_RES_ID, theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
if (verdict != null) {
return verdict;
}
@ -423,13 +423,13 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
if (target.getSearchParams() != null && !theFlags.contains(AuthorizationFlagsEnum.DO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
for (RuntimeSearchParam nextRuntimeSearchParam : params) {
String name = nextRuntimeSearchParam.getName();
Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(target.getSearchParams(), next, name, theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(target.getSearchParams(), next, name, theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
if (verdict != null) {
return verdict;
}
}
} else if (getMode() == PolicyEnum.ALLOW) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
}
}
@ -438,11 +438,11 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
if (!foundMatch) {
return null;
}
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
@Nullable
private Verdict applyRuleToInstances(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, RuleTarget target) {
private Verdict applyRuleToInstances(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, RuleTarget target, IRuleApplier theRuleApplier) {
if (target.resourceIds != null && target.resourceIds.size() > 0) {
int haveMatches = 0;
for (IIdType requestAppliesToResource : target.resourceIds) {
@ -463,7 +463,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
}
if (haveMatches == target.resourceIds.size()) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
}
@ -477,7 +477,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
}
if (theInputResource != null && requestAppliesToTransaction(ctx, myOp, theInputResource)) {
if (getMode() == PolicyEnum.DENY) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
List<BundleEntryParts> inputResources = BundleUtil.toListOfEntries(ctx, (IBaseBundle) theInputResource);
Verdict verdict = null;
@ -551,7 +551,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
* be applying security on the way out
*/
if (allComponentsAreGets) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
return verdict;
@ -602,7 +602,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
}
}
private Verdict checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(Map<String, String[]> theSearchParams, IIdType theCompartmentOwner, String theSearchParamName, RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) {
private Verdict checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(Map<String, String[]> theSearchParams, IIdType theCompartmentOwner, String theSearchParamName, RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier) {
Verdict verdict = null;
if (theSearchParams != null) {
String[] values = theSearchParams.get(theSearchParamName);
@ -611,11 +611,11 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
QualifiedParamList orParamList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextParameterValue);
for (String next : orParamList) {
if (next.equals(theCompartmentOwner.getValue())) {
verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
break;
}
if (next.equals(theCompartmentOwner.getIdPart())) {
verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
break;
}
}

View File

@ -41,7 +41,7 @@ class RuleImplPatch extends BaseRule {
if (myAllRequests) {
if (theOperation == RestOperationTypeEnum.PATCH) {
if (theInputResource == null && theOutputResource == null) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
}
}

View File

@ -41,7 +41,7 @@ public class RuleImplUpdateHistoryRewrite extends BaseRule {
IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags, Pointcut thePointcut) {
if (myAllRequests) {
if (theRequestDetails.getId() != null && theRequestDetails.getId().hasVersionIdPart() && theOperation == RestOperationTypeEnum.UPDATE) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
}
}

View File

@ -113,12 +113,12 @@ class SearchParameterAndValueSetRuleImpl extends RuleImplOp {
}
if (myWantCode && codeMatchCount.getMatchingCodeCount() > 0) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
} else if (!myWantCode) {
boolean notFound = getMode() == PolicyEnum.ALLOW && codeMatchCount.getMatchingCodeCount() == 0;
boolean othersFound = getMode() == PolicyEnum.DENY && codeMatchCount.getMatchingCodeCount() < codeMatchCount.getOverallCodeCount();
if (notFound || othersFound) {
AuthorizationInterceptor.Verdict verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
AuthorizationInterceptor.Verdict verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theRuleApplier);
if (notFound) {
troubleshootingLog
.debug("Code was not found in VS - Verdict: {}", verdict);

View File

@ -0,0 +1,103 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import ca.uhn.fhir.model.primitive.IdDt;
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.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static ca.uhn.fhir.rest.server.interceptor.auth.IAuthorizationSearchParamMatcher.MatchResult.buildMatched;
import static ca.uhn.fhir.rest.server.interceptor.auth.IAuthorizationSearchParamMatcher.MatchResult.buildUnmatched;
import static ca.uhn.fhir.rest.server.interceptor.auth.IAuthorizationSearchParamMatcher.MatchResult.buildUnsupported;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@MockitoSettings
class FhirQueryRuleTesterTest {
private static final Logger ourLog = LoggerFactory.getLogger(FhirQueryRuleTesterTest.class);
FhirQueryRuleTester myTester = new FhirQueryRuleTester("Observation", "code=foo");
IAuthRuleTester.RuleTestRequest myTestRequest;
@Mock
IBaseResource myResource;
@Mock
RequestDetails myRequestDetails;
@Mock
IRuleApplier myRuleApplier;
@Mock
IAuthorizationSearchParamMatcher mySearchParamMatcher;
@BeforeEach
void stubConfig() {
when(myRuleApplier.getSearchParamMatcher()).thenReturn(mySearchParamMatcher);
}
void stubMatchResult(IAuthorizationSearchParamMatcher.MatchResult result) {
when(mySearchParamMatcher.match("Observation?code=foo", myResource)).thenReturn(result);
}
private void stubLogForWarning() {
when(myRuleApplier.getTroubleshootingLog()).thenReturn(ourLog);
}
@Test
public void matchesFilter_true() {
myTestRequest = new IAuthRuleTester.RuleTestRequest(PolicyEnum.ALLOW, RestOperationTypeEnum.SEARCH_TYPE,
myRequestDetails, new IdDt("Observation/1"), myResource, myRuleApplier);
stubMatchResult(buildMatched());
boolean matches = myTester.matchesOutput(myTestRequest);
assertTrue(matches);
}
@Test
public void notMatchesFilter_false() {
//when(myRuleApplier.getSearchParamMatcher()).thenReturn(mySearchParamMatcher);
myTestRequest = new IAuthRuleTester.RuleTestRequest(PolicyEnum.ALLOW, RestOperationTypeEnum.SEARCH_TYPE,
myRequestDetails, new IdDt("Observation/1"), myResource, myRuleApplier);
stubMatchResult(buildUnmatched());
boolean matches = myTester.matchesOutput(myTestRequest);
assertFalse(matches);
}
@Test
public void unsupportedAllow_false() {
myTestRequest = new IAuthRuleTester.RuleTestRequest(PolicyEnum.ALLOW, RestOperationTypeEnum.SEARCH_TYPE,
myRequestDetails, new IdDt("Observation/1"), myResource, myRuleApplier);
stubMatchResult(buildUnsupported("a message"));
stubLogForWarning();
boolean matches = myTester.matchesOutput(myTestRequest);
assertFalse(matches);
}
@Test
public void unsupportedDeny_true() {
myTestRequest = new IAuthRuleTester.RuleTestRequest(PolicyEnum.DENY, RestOperationTypeEnum.SEARCH_TYPE,
myRequestDetails, new IdDt("Observation/1"), myResource, myRuleApplier);
stubMatchResult(buildUnsupported("a message"));
stubLogForWarning();
boolean matches = myTester.matchesOutput(myTestRequest);
assertTrue(matches);
}
}