Implement query filter auth via tester (#3758)
Use tester instead of new subclass to implement fhir filtering. The subclass was too complicated to be trusted for authorization.
This commit is contained in:
parent
835ec1b870
commit
7cf447cda1
|
@ -38,9 +38,9 @@ public class AuthorizationSearchParamMatcher implements IAuthorizationSearchPara
|
|||
}
|
||||
|
||||
@Override
|
||||
public MatchResult match(String theCriteria, IBaseResource theResource) {
|
||||
public MatchResult match(String theQueryParameters, IBaseResource theResource) {
|
||||
try {
|
||||
InMemoryMatchResult inMemoryMatchResult = mySearchParamMatcher.match(theCriteria, theResource, null);
|
||||
InMemoryMatchResult inMemoryMatchResult = mySearchParamMatcher.match(theQueryParameters, theResource, null);
|
||||
if (!inMemoryMatchResult.supported()) {
|
||||
return MatchResult.buildUnsupported(inMemoryMatchResult.getUnsupportedReason());
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ public class AuthorizationSearchParamMatcher implements IAuthorizationSearchPara
|
|||
// 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());
|
||||
ourLog.info("Unsupported filter {} applied to resource: {}", theQueryParameters, e.getMessage());
|
||||
return MatchResult.buildUnsupported(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,20 @@ package ca.uhn.fhir.jpa.auth;
|
|||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.FhirQueryRuleImpl;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.FhirQueryRuleTester;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthorizationSearchParamMatcher;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder;
|
||||
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||
import ca.uhn.test.util.LogbackCaptureTestExtension;
|
||||
import ch.qos.logback.classic.Level;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -25,15 +29,16 @@ import org.mockito.junit.jupiter.MockitoSettings;
|
|||
import javax.annotation.Nullable;
|
||||
import java.util.HashSet;
|
||||
|
||||
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.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
// wipjv where should this test live? -
|
||||
// wipjv can we mock the resource? We just use it for stubbing here. If so, move this back to hapi-fhir-server ca.uhn.fhir.rest.server.interceptor.auth
|
||||
// wipjv where should this test live? - It can't live in hapi-fhir-server since we need a real FhirContext for the compartment checks.
|
||||
@MockitoSettings
|
||||
class FhirQueryRuleImplTest implements ITestDataBuilder {
|
||||
|
||||
|
@ -47,57 +52,41 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
|
|||
@RegisterExtension
|
||||
LogbackCaptureTestExtension myLogCapture = new LogbackCaptureTestExtension(myMockRuleApplier.getTroubleshootingLog().getName());
|
||||
|
||||
private FhirQueryRuleImpl myRule;
|
||||
private IBaseResource myResource;
|
||||
private IBaseResource myResource2;
|
||||
//@Mock
|
||||
private final SystemRequestDetails mySrd = new SystemRequestDetails();
|
||||
private IAuthRule myRule;
|
||||
IIdType myPatientId = new IdDt("Patient/1");
|
||||
private IBaseResource myPatient;
|
||||
private IBaseResource myObservation;
|
||||
@Mock
|
||||
private RequestDetails myRequestDetails;
|
||||
private final FhirContext myFhirContext = FhirContext.forR4Cached();
|
||||
@Mock
|
||||
private IAuthorizationSearchParamMatcher myMatcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
mySrd.setFhirContext(myFhirContext);
|
||||
when(myRequestDetails.getFhirContext()).thenReturn(myFhirContext);
|
||||
}
|
||||
|
||||
|
||||
// // our IRuleApplierStubs
|
||||
// @Override
|
||||
// public Logger getTroubleshootingLog() {
|
||||
// return ourTargetLog;
|
||||
// }
|
||||
//
|
||||
// public IAuthorizationSearchParamMatcher getSearchParamMatcher() {
|
||||
// return myMatcher;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public AuthorizationInterceptor.Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Pointcut thePointcut) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
|
||||
@Nested
|
||||
public class MatchingLogic {
|
||||
|
||||
@Test
|
||||
public void simpleStringSearch_whenMatchResource_allow() {
|
||||
public void typeWithFilter_whenMatch_allow() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
|
||||
RuleBuilder b = new RuleBuilder();
|
||||
myRule = (FhirQueryRuleImpl) b.allow()
|
||||
myRule = b.allow()
|
||||
.read()
|
||||
.resourcesOfType("Patient")
|
||||
.withFilter( "family=Smith")
|
||||
.andThen().build().get(0);
|
||||
|
||||
when(myMatcher.match(ArgumentMatchers.eq("Patient?family=Smith"), ArgumentMatchers.same(myResource)))
|
||||
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildMatched());
|
||||
stubMatcherCall("Patient?family=Smith", myPatient, buildMatched());
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myPatient);
|
||||
|
||||
// then
|
||||
assertThat(verdict, notNullValue());
|
||||
|
@ -105,38 +94,74 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void simpleStringSearch_noMatch_noVerdict() {
|
||||
public void anyTypewithQueryFilter_whenMatch_allow() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
myRule = (FhirQueryRuleImpl) new RuleBuilder().allow().read().resourcesOfType("Patient")
|
||||
.inCompartmentWithFilter("patient", myResource.getIdElement().withResourceType("Patient"), "family=smi")
|
||||
|
||||
RuleBuilder b = new RuleBuilder();
|
||||
myRule = b.allow()
|
||||
.read()
|
||||
.allResources()
|
||||
.withFilter( "family=Smith")
|
||||
.andThen().build().get(0);
|
||||
when(myMatcher.match(ArgumentMatchers.eq("Patient?family=smi"), ArgumentMatchers.same(myResource)))
|
||||
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildUnmatched());
|
||||
|
||||
stubMatcherCall("Patient?family=Smith", myPatient, buildMatched());
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myPatient);
|
||||
|
||||
// then
|
||||
assertThat(verdict, notNullValue());
|
||||
assertThat(verdict.getDecision(), equalTo(PolicyEnum.ALLOW));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeWithQuery_noQueryMatch_noVerdict() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
myRule = new RuleBuilder().allow().read().resourcesOfType("Patient")
|
||||
.withFilter( "family=smi")
|
||||
.andThen().build().get(0);
|
||||
stubMatcherCall("Patient?family=smi", myPatient, buildUnmatched());
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myPatient);
|
||||
|
||||
// then
|
||||
assertThat(verdict, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void observation_notInCompartmentMatchFilter_noVerdict() {
|
||||
public void typeWithQuery_wrongType_noVerdict() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
withObservationWithSubjectAndCode(myPatientId);
|
||||
myRule = new RuleBuilder().allow().read().resourcesOfType("Patient")
|
||||
.withFilter( "family=smi")
|
||||
.andThen().build().get(0);
|
||||
//stubMatcherCall("Patient?family=smi", myPatient, buildUnmatched());
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myObservation);
|
||||
|
||||
// then
|
||||
assertThat(verdict, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inCompartmentwithQueryFilter_resourceNotInCompartmentButMatchFilter_noVerdict() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
// create patient for observation to point to so that the observation isn't in our main patient compartment
|
||||
IBaseResource patient = buildResource("Patient", withFamily("Jones"), withId("bad-id"));
|
||||
withObservationWithSubjectAndCode(patient.getIdElement());
|
||||
withObservationWithSubjectAndCode(myPatient.getIdElement());
|
||||
|
||||
myRule = (FhirQueryRuleImpl) new RuleBuilder().allow().read().resourcesOfType("Observation")
|
||||
.inCompartmentWithFilter("patient", myResource.getIdElement().withResourceType("Patient"), "code=28521000087105")
|
||||
myRule = new RuleBuilder().allow().read().resourcesOfType("Observation")
|
||||
.inCompartmentWithFilter("patient", myPatient.getIdElement().withResourceType("Patient"), "code=28521000087105")
|
||||
.andThen().build().get(0);
|
||||
when(myMatcher.match("Observation?code=28521000087105", myResource2))
|
||||
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildUnmatched());
|
||||
// matcher won't be called since not in compartment
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource2);
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myObservation);
|
||||
|
||||
// then
|
||||
assertThat(verdict, nullValue());
|
||||
|
@ -146,41 +171,88 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
|
|||
public void observation_noMatchFilter_noVerdict() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
withObservationWithSubjectAndCode(myResource.getIdElement());
|
||||
withObservationWithSubjectAndCode(myPatient.getIdElement());
|
||||
|
||||
myRule = (FhirQueryRuleImpl) new RuleBuilder().allow().read().resourcesOfType("Observation")
|
||||
myRule = new RuleBuilder().allow().read().resourcesOfType("Observation")
|
||||
.withFilter("code=12")
|
||||
.andThen().build().get(0);
|
||||
when(myMatcher.match("Observation?code=12", myResource2))
|
||||
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildUnmatched());
|
||||
|
||||
stubMatcherCall("Observation?code=12", myObservation, buildUnmatched());
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource2);
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myObservation);
|
||||
|
||||
// then
|
||||
assertThat(verdict, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void observation_denyWithFilter_deny() {
|
||||
public void denyTypeWithQueryFilter_match_deny() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
withObservationWithSubjectAndCode(myResource.getIdElement());
|
||||
withObservationWithSubjectAndCode(myPatient.getIdElement());
|
||||
|
||||
myRule = (FhirQueryRuleImpl) new RuleBuilder().deny().read().resourcesOfType("Observation")
|
||||
myRule = new RuleBuilder().deny().read().resourcesOfType("Observation")
|
||||
.withFilter("code=28521000087105")
|
||||
.andThen().build().get(0);
|
||||
when(myMatcher.match("Observation?code=28521000087105", myResource2))
|
||||
.thenReturn(IAuthorizationSearchParamMatcher.MatchResult.buildMatched());
|
||||
stubMatcherCall("Observation?code=28521000087105", myObservation, buildMatched());
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource2);
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myObservation);
|
||||
|
||||
// then
|
||||
assertThat(verdict, notNullValue());
|
||||
assertThat(verdict.getDecision(), equalTo(PolicyEnum.DENY));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void allowIdwithQueryFilter_matchesIdAndFilter_allow() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
|
||||
myRule = new RuleBuilder()
|
||||
.allow()
|
||||
.read().instance(myPatient.getIdElement())
|
||||
.withTester(new FhirQueryRuleTester("name=smith"))
|
||||
.andThen().build().get(0);
|
||||
|
||||
stubMatcherCall("Patient?name=smith", myPatient, buildMatched());
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myPatient);
|
||||
|
||||
// then
|
||||
assertThat(verdict, notNullValue());
|
||||
assertThat(verdict.getDecision(), equalTo(PolicyEnum.ALLOW));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void allowIdwithQueryFilter_matchesJustIdNotFilter_abstain() {
|
||||
// given
|
||||
withPatientWithNameAndId();
|
||||
|
||||
myRule = new RuleBuilder()
|
||||
.allow()
|
||||
.read().instance(myPatient.getIdElement())
|
||||
.withTester(new FhirQueryRuleTester("name=smith"))
|
||||
.andThen().build().get(0);
|
||||
|
||||
stubMatcherCall("Patient?name=smith", myPatient, buildUnmatched());
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myPatient);
|
||||
|
||||
// then
|
||||
assertThat(verdict, nullValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void stubMatcherCall(String expectedQuery, IBaseResource theTargetResource, IAuthorizationSearchParamMatcher.MatchResult theStubResult) {
|
||||
when(myMatcher.match(ArgumentMatchers.eq(expectedQuery), ArgumentMatchers.same(theTargetResource)))
|
||||
.thenReturn(theStubResult);
|
||||
}
|
||||
|
||||
|
||||
|
@ -197,35 +269,33 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
|
|||
@Test
|
||||
public void givenAllowRule_whenUnsupportedQuery_noVerdict() {
|
||||
withPatientWithNameAndId();
|
||||
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.buildUnsupported("I'm broken unsupported chain XXX"));
|
||||
myRule = new RuleBuilder().allow().read().resourcesOfType("Patient")
|
||||
.inCompartmentWithFilter("patient", myPatient.getIdElement().withResourceType("Patient"), "unsupported.chain=smi").andThen().build().get(0);
|
||||
stubMatcherCall("Patient?unsupported.chain=smi", myPatient, buildUnsupported("I'm broken unsupported chain XXX"));
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myPatient);
|
||||
|
||||
// then
|
||||
assertThat(verdict, nullValue());
|
||||
assertThat(myLogCapture.getLogEvents(),
|
||||
hasItem(myLogCapture.eventWithLevelAndMessageContains(Level.WARN, "unsupported chain XXX")));
|
||||
MatcherAssert.assertThat(myLogCapture.getLogEvents(),
|
||||
Matchers.hasItem(myLogCapture.eventWithLevelAndMessageContains(Level.WARN, "unsupported chain XXX")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenDenyRule_whenUnsupportedQuery_reject() {
|
||||
withPatientWithNameAndId();
|
||||
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.buildUnsupported("I'm broken unsupported chain XXX"));
|
||||
myRule = new RuleBuilder().deny().read().resourcesOfType("Patient")
|
||||
.inCompartmentWithFilter("patient", myPatientId, "unsupported.chain=smi").andThen().build().get(0);
|
||||
stubMatcherCall("Patient?unsupported.chain=smi", myPatient, buildUnsupported("I'm broken unsupported chain XXX"));
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myPatient);
|
||||
|
||||
// then
|
||||
assertThat(verdict.getDecision(), equalTo(PolicyEnum.DENY));
|
||||
assertThat(myLogCapture.getLogEvents(),
|
||||
hasItem(myLogCapture.eventWithLevelAndMessageContains(Level.WARN, "unsupported chain XXX")));
|
||||
MatcherAssert.assertThat(myLogCapture.getLogEvents(),
|
||||
Matchers.hasItem(myLogCapture.eventWithLevelAndMessageContains(Level.WARN, "unsupported chain XXX")));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,16 +306,16 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
|
|||
public void noMatcherService_unsupportedPerm_noVerdict() {
|
||||
withPatientWithNameAndId();
|
||||
myMatcher = null;
|
||||
myRule = (FhirQueryRuleImpl) new RuleBuilder().allow().read().resourcesOfType("Patient")
|
||||
.inCompartmentWithFilter("patient", myResource.getIdElement().withResourceType("Observation"), "code:in=foo").andThen().build().get(0);
|
||||
myRule = new RuleBuilder().allow().read().resourcesOfType("Patient")
|
||||
.inCompartmentWithFilter("patient", myPatient.getIdElement().withResourceType("Patient"), "code:in=foo").andThen().build().get(0);
|
||||
|
||||
// when
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myResource);
|
||||
AuthorizationInterceptor.Verdict verdict = applyRuleToResource(myPatient);
|
||||
|
||||
// then
|
||||
assertThat(verdict, nullValue());
|
||||
assertThat(myLogCapture.getLogEvents(),
|
||||
hasItem(myLogCapture.eventWithLevelAndMessageContains(Level.WARN, "No matcher provided")));
|
||||
MatcherAssert.assertThat(myLogCapture.getLogEvents(),
|
||||
Matchers.hasItem(myLogCapture.eventWithLevelAndMessageContains(Level.WARN, "No matcher provided")));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -253,20 +323,21 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
|
|||
// We need the builder to set AppliesTypeEnum, and the use that to build the matcher expression.
|
||||
|
||||
private AuthorizationInterceptor.Verdict applyRuleToResource(IBaseResource theResource) {
|
||||
return myRule.applyRule(RestOperationTypeEnum.SEARCH_TYPE, mySrd, null, null, theResource, myMockRuleApplier, new HashSet<>(), Pointcut.STORAGE_PRESHOW_RESOURCES);
|
||||
return myRule.applyRule(RestOperationTypeEnum.SEARCH_TYPE, myRequestDetails, null, null, theResource, myMockRuleApplier, new HashSet<>(), Pointcut.STORAGE_PRESHOW_RESOURCES);
|
||||
}
|
||||
|
||||
private void withPatientWithNameAndId() {
|
||||
myResource = buildResource("Patient", withFamily("Smith"), withId("some-id"));
|
||||
myPatient = buildPatient(withId(myPatientId));
|
||||
}
|
||||
|
||||
// Use in sequence with above
|
||||
private void withObservationWithSubjectAndCode(IIdType theIdElement) {
|
||||
String snomedUriString = "http://snomed.info/sct";
|
||||
String snomedUriString = "https://snomed.info/sct";
|
||||
String insulin2hCode = "28521000087105";
|
||||
myResource2 = buildResource("Observation", withObservationCode(snomedUriString, insulin2hCode), withSubject(theIdElement));
|
||||
myObservation = buildResource("Observation", withObservationCode(snomedUriString, insulin2hCode), withSubject(theIdElement));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public IIdType doCreateResource(IBaseResource theResource) {
|
||||
return null;
|
||||
|
@ -279,6 +350,7 @@ class FhirQueryRuleImplTest implements ITestDataBuilder {
|
|||
|
||||
@Override
|
||||
public FhirContext getFhirContext() {
|
||||
return FhirContext.forR4Cached();
|
||||
return myFhirContext;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1765,8 +1765,9 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
|
|||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow("filter rule").read().allResources().withFilter("code=" + FhirResourceDaoR4TerminologyTest.URL_MY_CODE_SYSTEM + "|").andThen()
|
||||
.build();
|
||||
.allow("filter rule").read().allResources().withAnyId()
|
||||
.withFilterTester("code=" + FhirResourceDaoR4TerminologyTest.URL_MY_CODE_SYSTEM + "|")
|
||||
.andThen().build();
|
||||
}
|
||||
};
|
||||
interceptor.setAuthorizationSearchParamMatcher(new AuthorizationSearchParamMatcher(mySearchParamMatcher));
|
||||
|
|
|
@ -25,6 +25,8 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
|||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
|
@ -112,4 +114,16 @@ abstract class BaseRule implements IAuthRule {
|
|||
return thePointcut.equals(Pointcut.STORAGE_PREACCESS_RESOURCES) || thePointcut.equals(Pointcut.STORAGE_PRESHOW_RESOURCES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
ToStringBuilder builder = toStringBuilder();
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
ToStringBuilder toStringBuilder() {
|
||||
ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
builder.append("testers", myTesters);
|
||||
return builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor.auth;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Server Framework
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Extension to rules that also requires matching a query filter, e.g. code=foo
|
||||
*/
|
||||
public class FhirQueryRuleImpl extends RuleImplOp {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(FhirQueryRuleImpl.class);
|
||||
|
||||
private String myFilter;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public FhirQueryRuleImpl(String theRuleName) {
|
||||
super(theRuleName);
|
||||
}
|
||||
|
||||
public void setFilter(String theFilter) {
|
||||
myFilter = theFilter;
|
||||
}
|
||||
|
||||
public String getFilter() {
|
||||
return myFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Our override that first checks our filter against the resource if present.
|
||||
*/
|
||||
@Override
|
||||
protected AuthorizationInterceptor.Verdict applyRuleLogic(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Set<AuthorizationFlagsEnum> theFlags, FhirContext theFhirContext, RuleTarget theRuleTarget, IRuleApplier theRuleApplier) {
|
||||
ourLog.trace("applyRuleLogic {} {}", theOperation, theRuleTarget);
|
||||
// Note - theOutputResource == null means we're in some other pointcut and don't have a result yet.
|
||||
if (theOutputResource == null) {
|
||||
return super.applyRuleLogic(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theFlags, theFhirContext, theRuleTarget, theRuleApplier);
|
||||
}
|
||||
|
||||
// look for a matcher
|
||||
IAuthorizationSearchParamMatcher matcher = theRuleApplier.getSearchParamMatcher();
|
||||
if (matcher == null) {
|
||||
theRuleApplier.getTroubleshootingLog().warn("No matcher provided. Can't apply filter permission.");
|
||||
if ( PolicyEnum.DENY.equals(getMode())) {
|
||||
return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// wipjv check in vs out resource - write will need to check write.
|
||||
// wipjv what about the id case - does that path doesn't call applyRuleLogic()
|
||||
IAuthorizationSearchParamMatcher.MatchResult mr = matcher.match(theOutputResource.fhirType() + "?" + myFilter, theOutputResource);
|
||||
|
||||
AuthorizationInterceptor.Verdict result;
|
||||
switch (mr.getMatch()) {
|
||||
case MATCH:
|
||||
result = super.applyRuleLogic(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theFlags, theFhirContext, theRuleTarget, theRuleApplier);
|
||||
break;
|
||||
case UNSUPPORTED:
|
||||
theRuleApplier.getTroubleshootingLog().warn("Unsupported matcher expression {}: {}. Abstaining.", myFilter, mr.getUnsupportedReason());
|
||||
if ( PolicyEnum.DENY.equals(getMode())) {
|
||||
result = new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this);
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
break;
|
||||
case NO_MATCH:
|
||||
default:
|
||||
result = null;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
protected ToStringBuilder toStringBuilder() {
|
||||
return super.toStringBuilder()
|
||||
.append("filter", myFilter);
|
||||
}
|
||||
}
|
|
@ -1,37 +1,52 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor.auth;
|
||||
|
||||
public class FhirQueryRuleTester implements IAuthRuleTester {
|
||||
private final String myType;
|
||||
private final String myFilter;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
public FhirQueryRuleTester(String theType, String theFilter) {
|
||||
myType = theType;
|
||||
myFilter = theFilter;
|
||||
/**
|
||||
* Tester that a resource matches a provided query filter.
|
||||
*/
|
||||
public class FhirQueryRuleTester implements IAuthRuleTester {
|
||||
private final String myQueryParameters;
|
||||
|
||||
public FhirQueryRuleTester(String theQueryParameters) {
|
||||
myQueryParameters = theQueryParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(RuleTestRequest theRuleTestRequest) {
|
||||
// wipmb placeholder until we get to writes
|
||||
return true;
|
||||
return checkMatch(theRuleTestRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchesOutput(RuleTestRequest theRuleTestRequest) {
|
||||
return checkMatch(theRuleTestRequest);
|
||||
}
|
||||
|
||||
private boolean checkMatch(RuleTestRequest theRuleTestRequest) {
|
||||
// look for a matcher
|
||||
IAuthorizationSearchParamMatcher matcher = theRuleTestRequest.ruleApplier.getSearchParamMatcher();
|
||||
if (matcher == null) {
|
||||
theRuleTestRequest.ruleApplier.getTroubleshootingLog().warn("No matcher provided. Can't apply filter permission.");
|
||||
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);
|
||||
// this is a bit weird.
|
||||
// A tester narrows a rule -- i.e. a rule only applies if the main logic matches AND the testers all match
|
||||
// But this rule would have matched without this tester, so true means abstain.
|
||||
if (theRuleTestRequest.resource == null) {
|
||||
// we aren't looking at a resource yet. treat as no-op
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (mr.getMatch()) {
|
||||
// we use the target type since the rule might apply to all types, a type set, or instances, and that has already been checked.
|
||||
IAuthorizationSearchParamMatcher.MatchResult mr = matcher.match(theRuleTestRequest.resource.fhirType() + "?" + myQueryParameters, theRuleTestRequest.resource);
|
||||
|
||||
switch (mr.match) {
|
||||
case MATCH:
|
||||
return true;
|
||||
case UNSUPPORTED:
|
||||
theRuleTestRequest.ruleApplier.getTroubleshootingLog().warn("Unsupported matcher expression {}: {}.", myFilter, mr.getUnsupportedReason());
|
||||
theRuleTestRequest.ruleApplier.getTroubleshootingLog().warn("Unsupported matcher expression {}: {}.", myQueryParameters, mr.unsupportedReason);
|
||||
// unsupported doesn't match unless this is a deny request, and we need to be safe!
|
||||
return (theRuleTestRequest.mode == PolicyEnum.DENY);
|
||||
case NO_MATCH:
|
||||
|
@ -39,4 +54,11 @@ public class FhirQueryRuleTester implements IAuthRuleTester {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
|
||||
.append("filter", myQueryParameters)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,4 +51,10 @@ public interface IAuthRuleFinished {
|
|||
*/
|
||||
IAuthRuleFinished withTester(@Nullable IAuthRuleTester theTester);
|
||||
|
||||
/**
|
||||
* Narrow this rule to resources matching the given FHIR query.
|
||||
* @param theQueryParameters a FHIR query parameter string. E.g. category=laboratory&date=ge2021
|
||||
*/
|
||||
IAuthRuleFinished withFilterTester(String theQueryParameters);
|
||||
|
||||
}
|
||||
|
|
|
@ -78,12 +78,18 @@ public interface IAuthRuleTester {
|
|||
}
|
||||
|
||||
/**
|
||||
* Allows user-supplied logic for authorization rules.
|
||||
* User supplied logic called just before the parent rule renders a verdict on the operation
|
||||
* or input resource.
|
||||
*
|
||||
* Returning true will allow the verdict continue.
|
||||
* Returning false will block the verdict and cause the rule to abstain (i.e. return null).
|
||||
*
|
||||
* <p>
|
||||
* THIS IS AN EXPERIMENTAL API! Feedback is welcome, and this API
|
||||
* may change.
|
||||
*
|
||||
* @param theRequest The details to evaluate
|
||||
* @param theRequest The details of the operation or an INPUT resource to evaluate
|
||||
* @return true if the verdict should continue
|
||||
* @since 6.1.0
|
||||
*/
|
||||
default boolean matches(RuleTestRequest theRequest) {
|
||||
|
@ -101,14 +107,18 @@ public interface IAuthRuleTester {
|
|||
}
|
||||
|
||||
/**
|
||||
* Allows user-supplied logic for authorization rules.
|
||||
* User supplied logic called just before the parent rule renders a verdict on an output resource.
|
||||
*
|
||||
* Returning true will allow the verdict continue.
|
||||
* Returning false will block the verdict and cause the rule to abstain (i.e. return null).
|
||||
*
|
||||
* <p>
|
||||
* THIS IS AN EXPERIMENTAL API! Feedback is welcome, and this API
|
||||
* may change.
|
||||
*
|
||||
* @param theRequest The details to evaluate
|
||||
* @since 6.1.0
|
||||
*/
|
||||
* @param theRequest The details of the operation or an INPUT resource to evaluate
|
||||
* @return true if the verdict should continue
|
||||
* @since 6.1.0 */
|
||||
default boolean matchesOutput(RuleTestRequest theRequest) {
|
||||
return this.matchesOutput(theRequest.operation, theRequest.requestDetails, theRequest.resource);
|
||||
}
|
||||
|
|
|
@ -26,10 +26,17 @@ import javax.annotation.Nonnull;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Adapt the InMemoryMatcher to support authorization filters in {@link FhirQueryRuleImpl}.
|
||||
* Adapt the InMemoryMatcher to support authorization filters in {@link FhirQueryRuleTester}.
|
||||
* Exists because filters may be applied to resources that don't support all paramters, and UNSUPPORTED
|
||||
* has a different meaning during authorization.
|
||||
*/
|
||||
public interface IAuthorizationSearchParamMatcher {
|
||||
MatchResult match(String theCriteria, IBaseResource theResource);
|
||||
/**
|
||||
* Calculate if the resource would match the fhir query parameters.
|
||||
* @param theQueryParameters e.g. "category=laboratory"
|
||||
* @param theResource the target of the comparison
|
||||
*/
|
||||
MatchResult match(String theQueryParameters, IBaseResource theResource);
|
||||
|
||||
/**
|
||||
* Match outcomes.
|
||||
|
@ -42,9 +49,11 @@ public interface IAuthorizationSearchParamMatcher {
|
|||
}
|
||||
|
||||
class MatchResult {
|
||||
// wipmb consider a record pattern - public and drop the accessors.
|
||||
@Nonnull private final Match myMatch;
|
||||
@Nullable private final String myUnsupportedReason;
|
||||
// fake record pattern
|
||||
/** match result */
|
||||
@Nonnull public final Match match;
|
||||
/** the reason for the UNSUPPORTED result */
|
||||
@Nullable public final String unsupportedReason;
|
||||
|
||||
public static MatchResult buildMatched() {
|
||||
return new MatchResult(Match.MATCH, null);
|
||||
|
@ -59,16 +68,8 @@ public interface IAuthorizationSearchParamMatcher {
|
|||
}
|
||||
|
||||
private MatchResult(Match myMatch, String myUnsupportedReason) {
|
||||
this.myMatch = myMatch;
|
||||
this.myUnsupportedReason = myUnsupportedReason;
|
||||
}
|
||||
|
||||
public Match getMatch() {
|
||||
return myMatch;
|
||||
}
|
||||
|
||||
public String getUnsupportedReason() {
|
||||
return myUnsupportedReason;
|
||||
this.match = myMatch;
|
||||
this.unsupportedReason = myUnsupportedReason;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -185,6 +185,11 @@ public class RuleBuilder implements IAuthRuleBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAuthRuleFinished withFilterTester(String theQueryParameters) {
|
||||
return withTester(new FhirQueryRuleTester(theQueryParameters));
|
||||
}
|
||||
|
||||
private class TenantCheckingTester implements IAuthRuleTester {
|
||||
private final Collection<String> myTenantIds;
|
||||
private final boolean myOutcome;
|
||||
|
@ -600,17 +605,19 @@ public class RuleBuilder implements IAuthRuleBuilder {
|
|||
}
|
||||
myInCompartmentOwners = Collections.singletonList(theIdElement);
|
||||
|
||||
FhirQueryRuleImpl rule = new FhirQueryRuleImpl(myRuleName);
|
||||
rule.setFilter(theFilter);
|
||||
return finished(rule);
|
||||
RuleBuilderFinished result = finished();
|
||||
result.withTester(new FhirQueryRuleTester(theFilter));
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAuthRuleFinished withFilter(String theFilter) {
|
||||
myClassifierType = ClassifierTypeEnum.ANY_ID;
|
||||
FhirQueryRuleImpl rule = new FhirQueryRuleImpl(myRuleName);
|
||||
rule.setFilter(theFilter);
|
||||
return finished(rule);
|
||||
|
||||
RuleBuilderFinished result = finished();
|
||||
result.withTester(new FhirQueryRuleTester(theFilter));
|
||||
return result;
|
||||
}
|
||||
|
||||
RuleBuilderFinished addInstances(Collection<IIdType> theInstances) {
|
||||
|
|
|
@ -675,15 +675,9 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
ToStringBuilder builder = toStringBuilder();
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected ToStringBuilder toStringBuilder() {
|
||||
ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
ToStringBuilder builder = super.toStringBuilder();
|
||||
builder.append("op", myOp);
|
||||
builder.append("transactionAppliesToOp", myTransactionAppliesToOp);
|
||||
builder.append("appliesTo", myAppliesTo);
|
||||
|
|
|
@ -16,16 +16,18 @@ import static ca.uhn.fhir.rest.server.interceptor.auth.IAuthorizationSearchParam
|
|||
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.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
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");
|
||||
FhirQueryRuleTester myTester = new FhirQueryRuleTester("code=foo");
|
||||
|
||||
IAuthRuleTester.RuleTestRequest myTestRequest;
|
||||
@Mock
|
||||
IBaseResource myResource;
|
||||
IBaseResource myObservation;
|
||||
@Mock
|
||||
RequestDetails myRequestDetails;
|
||||
@Mock
|
||||
|
@ -35,11 +37,13 @@ class FhirQueryRuleTesterTest {
|
|||
|
||||
@BeforeEach
|
||||
void stubConfig() {
|
||||
when(myRuleApplier.getSearchParamMatcher()).thenReturn(mySearchParamMatcher);
|
||||
lenient().when(myRuleApplier.getSearchParamMatcher()).thenReturn(mySearchParamMatcher);
|
||||
lenient().when(myObservation.fhirType()).thenReturn("Observation");
|
||||
|
||||
}
|
||||
|
||||
void stubMatchResult(IAuthorizationSearchParamMatcher.MatchResult result) {
|
||||
when(mySearchParamMatcher.match("Observation?code=foo", myResource)).thenReturn(result);
|
||||
when(mySearchParamMatcher.match("Observation?code=foo", myObservation)).thenReturn(result);
|
||||
}
|
||||
|
||||
private void stubLogForWarning() {
|
||||
|
@ -51,7 +55,7 @@ class FhirQueryRuleTesterTest {
|
|||
public void matchesFilter_true() {
|
||||
|
||||
myTestRequest = new IAuthRuleTester.RuleTestRequest(PolicyEnum.ALLOW, RestOperationTypeEnum.SEARCH_TYPE,
|
||||
myRequestDetails, new IdDt("Observation/1"), myResource, myRuleApplier);
|
||||
myRequestDetails, new IdDt("Observation/1"), myObservation, myRuleApplier);
|
||||
stubMatchResult(buildMatched());
|
||||
|
||||
boolean matches = myTester.matchesOutput(myTestRequest);
|
||||
|
@ -62,10 +66,9 @@ class FhirQueryRuleTesterTest {
|
|||
|
||||
@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);
|
||||
myRequestDetails, new IdDt("Observation/1"), myObservation, myRuleApplier);
|
||||
stubMatchResult(buildUnmatched());
|
||||
|
||||
boolean matches = myTester.matchesOutput(myTestRequest);
|
||||
|
@ -77,7 +80,7 @@ class FhirQueryRuleTesterTest {
|
|||
public void unsupportedAllow_false() {
|
||||
|
||||
myTestRequest = new IAuthRuleTester.RuleTestRequest(PolicyEnum.ALLOW, RestOperationTypeEnum.SEARCH_TYPE,
|
||||
myRequestDetails, new IdDt("Observation/1"), myResource, myRuleApplier);
|
||||
myRequestDetails, new IdDt("Observation/1"), myObservation, myRuleApplier);
|
||||
stubMatchResult(buildUnsupported("a message"));
|
||||
stubLogForWarning();
|
||||
|
||||
|
@ -90,7 +93,7 @@ class FhirQueryRuleTesterTest {
|
|||
public void unsupportedDeny_true() {
|
||||
|
||||
myTestRequest = new IAuthRuleTester.RuleTestRequest(PolicyEnum.DENY, RestOperationTypeEnum.SEARCH_TYPE,
|
||||
myRequestDetails, new IdDt("Observation/1"), myResource, myRuleApplier);
|
||||
myRequestDetails, new IdDt("Observation/1"), myObservation, myRuleApplier);
|
||||
stubMatchResult(buildUnsupported("a message"));
|
||||
stubLogForWarning();
|
||||
|
||||
|
@ -99,5 +102,17 @@ class FhirQueryRuleTesterTest {
|
|||
assertTrue(matches);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preHandledCheckHasNoResource_true() {
|
||||
|
||||
myTestRequest = new IAuthRuleTester.RuleTestRequest(PolicyEnum.DENY, RestOperationTypeEnum.READ,
|
||||
myRequestDetails, null, null, myRuleApplier);
|
||||
// no stubs needed since we don't have a resource
|
||||
|
||||
boolean matches = myTester.matchesOutput(myTestRequest);
|
||||
|
||||
assertTrue(matches);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ public class RuleImplOpTest {
|
|||
|
||||
@Test
|
||||
public void testToString() {
|
||||
assertEquals("RuleImplOp[op=<null>,transactionAppliesToOp=<null>,appliesTo=<null>,appliesToTypes=<null>,classifierCompartmentName=<null>,classifierCompartmentOwners=<null>,classifierType=<null>]", new RuleImplOp("").toString());
|
||||
assertEquals("RuleImplOp[testers=<null>,op=<null>,transactionAppliesToOp=<null>,appliesTo=<null>,appliesToTypes=<null>,classifierCompartmentName=<null>,classifierCompartmentOwners=<null>,classifierType=<null>]", new RuleImplOp("").toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in New Issue