Compare commits

...

5 Commits

Author SHA1 Message Date
James Agnew 62c8f94f11
Merge 703924938f into 3f6d1eb29b 2024-09-26 02:08:11 +00:00
Thomas Papke 3f6d1eb29b
#5768 Upgrade to latest simple-java-mail (#6261) 2024-09-26 02:07:27 +00:00
Tadgh 377e44b6ca
attribution and pom change (#6309) 2024-09-25 20:38:22 +00:00
James Agnew 703924938f Cleanup 2024-09-10 10:02:52 -04:00
James Agnew 7ad2524272 Fix #6258 - Improve auth interceptor operation handling 2024-09-10 10:00:27 -04:00
17 changed files with 319 additions and 195 deletions

View File

@ -0,0 +1,10 @@
---
type: fix
issue: 6258
title: "The AuthorizationInterceptor handling for operations has been improved
so that operation rules now directly test the contents of response Bundle
or Parameters objects returned by the operation when configure to require
explicit response authorization. This fixes a regression in 7.4.0 where
operation responses could sometimes be denied even if appropriate
permissions were granted to view resources in a response bundle. Thanks to
Gijsbert van den Brink for reporting the issue with a sample test!"

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 6290
title: "Previously, a specific migration task was using the `TRIM()` function, which does not exist in MSSQL 2012. This was causing migrations targeting MSSQL 2012 to fail.
This has been corrected and replaced with usage of a combination of LTRIM() and RTRIM(). Thanks to Primož Delopst at Better for the contribution!"

View File

@ -70,6 +70,11 @@
<artifactId>jakarta.servlet-api</artifactId> <artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
<optional>true</optional>
</dependency>
<!-- test dependencies --> <!-- test dependencies -->
<dependency> <dependency>

View File

@ -28,8 +28,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import javax.mail.internet.InternetAddress; import jakarta.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;

View File

@ -17,8 +17,8 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.mail.internet.InternetAddress; import jakarta.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import java.util.Arrays; import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;

View File

@ -26,8 +26,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import javax.mail.internet.InternetAddress; import jakarta.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;

View File

@ -20,11 +20,15 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
import ca.uhn.fhir.rest.gclient.IOperation;
import ca.uhn.fhir.rest.gclient.IOperationUnnamed;
import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor; import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule; import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule;
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRuleBuilder;
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRuleTester; import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRuleTester;
import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum; import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum;
import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder; import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder;
@ -68,6 +72,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
@ -897,8 +902,17 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
} }
@Test @ParameterizedTest
public void testDiffOperation_AllowedByType_Instance() { @CsvSource({
// ResourceId , RequireExplicitResponseAuthorization , ShouldSucceed
"Patient/A , true , true",
"Patient/A/_history/2 , true , true",
"Observation/B , true , false",
"Patient/A , false , true",
"Patient/A/_history/2 , false , true",
"Observation/B , false , true"
})
public void testDiffOperation_AllowedByType_Instance(String theResourceId, boolean theRequireExplicitResponseAuthorization, boolean theShouldSucceed) {
createPatient(withId("A"), withActiveTrue()); createPatient(withId("A"), withActiveTrue());
createPatient(withId("A"), withActiveFalse()); createPatient(withId("A"), withActiveFalse());
createObservation(withId("B"), withStatus("final")); createObservation(withId("B"), withStatus("final"));
@ -906,30 +920,42 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
myServer.getRestfulServer().registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { myServer.getRestfulServer().registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() RuleBuilder ruleBuilder = new RuleBuilder();
.allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onAnyInstance().andAllowAllResponses().andThen() if (theRequireExplicitResponseAuthorization) {
.allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onServer().andAllowAllResponses().andThen() ruleBuilder.allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onAnyInstance().andRequireExplicitResponseAuthorization().andThen();
.allow().read().resourcesOfType(Patient.class).withAnyId().andThen() ruleBuilder.allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onServer().andRequireExplicitResponseAuthorization().andThen();
.denyAll() } else {
.build(); ruleBuilder.allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onAnyInstance().andAllowAllResponses().andThen();
ruleBuilder.allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onServer().andAllowAllResponses().andThen();
}
ruleBuilder.allow().read().resourcesOfType(Patient.class).withAnyId().andThen();
ruleBuilder.denyAll();
return ruleBuilder.build();
} }
}); });
Parameters diff; Parameters diff;
diff = myClient.operation().onInstance("Patient/A").named(ProviderConstants.DIFF_OPERATION_NAME).withNoParameters(Parameters.class).execute(); IOperation operation = myClient.operation();
assertThat(diff.getParameter()).hasSize(1); IOperationUnnamed target;
if (theResourceId.contains("_history")) {
diff = myClient.operation().onInstanceVersion(new IdType("Patient/A/_history/2")).named(ProviderConstants.DIFF_OPERATION_NAME).withNoParameters(Parameters.class).execute(); target = operation.onInstanceVersion(new IdType(theResourceId));
assertThat(diff.getParameter()).hasSize(1); } else {
target = operation.onInstance(theResourceId);
try {
myClient.operation().onInstance("Observation/B").named(ProviderConstants.DIFF_OPERATION_NAME).withNoParameters(Parameters.class).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
} }
IOperationUntypedWithInput<Parameters> executable = target.named(ProviderConstants.DIFF_OPERATION_NAME).withNoParameters(Parameters.class);
if (theShouldSucceed) {
diff = executable.execute();
assertThat(diff.getParameter()).hasSize(1);
} else {
try {
executable.execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
} }
@Test @Test
@ -943,8 +969,8 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() return new RuleBuilder()
.allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onAnyInstance().andAllowAllResponses().andThen() .allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onAnyInstance().andRequireExplicitResponseAuthorization().andThen()
.allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onServer().andAllowAllResponses().andThen() .allow().operation().named(ProviderConstants.DIFF_OPERATION_NAME).onServer().andRequireExplicitResponseAuthorization().andThen()
.allow().read().resourcesOfType(Patient.class).withAnyId().andThen() .allow().read().resourcesOfType(Patient.class).withAnyId().andThen()
.denyAll() .denyAll()
.build(); .build();
@ -977,6 +1003,72 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
} }
@Test
public void testDocumentOperation_withExplicitAuthorization() {
IIdType patientId = createPatient();
IIdType compositionId = createResource("Composition", withSubject(patientId));
AuthorizationInterceptor interceptor = new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().read().instance(patientId).andThen()
.allow().operation().named("$document").onInstance(compositionId).andRequireExplicitResponseAuthorization().andThen()
.allow().read().instance(compositionId).andThen()
.denyAll()
.build();
}
};
myServer.getRestfulServer().registerInterceptor(interceptor);
Bundle bundle = myClient.operation().onInstanceVersion(compositionId).named("$document").withNoParameters(Parameters.class).returnResourceType(Bundle.class).execute();
assertEquals(2, bundle.getEntry().size());
}
@Test
public void testDocumentOperation_explicitAuthorizationNotNeeded() {
IIdType patientId = createPatient();
IIdType compositionId = createResource("Composition", withSubject(patientId));
AuthorizationInterceptor interceptor = new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().operation().named("$document").onInstance(compositionId).andAllowAllResponses().andThen()
.denyAll()
.build();
}
};
myServer.getRestfulServer().registerInterceptor(interceptor);
Bundle bundle = myClient.operation().onInstanceVersion(compositionId).named("$document").withNoParameters(Parameters.class).returnResourceType(Bundle.class).execute();
assertEquals(2, bundle.getEntry().size());
}
@Test
public void testDocumentOperation_withoutExplicitAuthorization() {
IIdType patientId = createPatient();
IIdType compositionId = createResource("Composition", withSubject(patientId));
AuthorizationInterceptor interceptor = new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().operation().named("$document").onInstance(compositionId).andRequireExplicitResponseAuthorization().andThen()
.allow().read().instance(compositionId).andThen()
.denyAll()
.build();
}
};
myServer.getRestfulServer().registerInterceptor(interceptor);
try {
myClient.operation().onInstanceVersion(compositionId).named("$document").withNoParameters(Parameters.class).returnResourceType(Bundle.class).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
@Test @Test
public void testGraphQL_AllowedByType_Instance() throws IOException { public void testGraphQL_AllowedByType_Instance() throws IOException {
@ -1205,8 +1297,17 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp)); ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp));
} }
@Test @ParameterizedTest
public void testOperationEverything_SomeIncludedResourcesNotAuthorized() { @CsvSource({
// RequireExplicitResponseAuthorization , AddNotExplicitlyAuthorizedResources , ShouldSucceed
"true , false , true",
"true , true , false",
"false , false , true",
"false , true , true",
})
public void testOperationEverything_SomeIncludedResourcesNotAuthorized(boolean theRequireExplicitResponseAuthorization, boolean theAddNotExplicitlyAuthorizedResources, boolean theShouldSucceed) {
int expectedCount = 2;
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.setActive(true); pt1.setActive(true);
final IIdType pid1 = myClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless(); final IIdType pid1 = myClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless();
@ -1216,45 +1317,57 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
obs1.setSubject(new Reference(pid1)); obs1.setSubject(new Reference(pid1));
myClient.create().resource(obs1).execute(); myClient.create().resource(obs1).execute();
if (theAddNotExplicitlyAuthorizedResources) {
// Add an Encounter, which will be returned by $everything but that hasn't been
// explicitly authorized
Encounter enc = new Encounter();
enc.setSubject(new Reference(pid1));
myClient.create().resource(enc).execute();
Organization org = new Organization();
org.setName("Hello");
IIdType orgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless();
expectedCount++;
pt1.setId(pid1);
pt1.setManagingOrganization(new Reference(orgId));
myClient.update().resource(pt1).execute();
expectedCount++;
}
myServer.getRestfulServer().registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { myServer.getRestfulServer().registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@SuppressWarnings("deprecation")
@Override @Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder() IAuthRuleBuilder ruleBuilder = new RuleBuilder();
.allow().operation().named(JpaConstants.OPERATION_EVERYTHING).onInstance(pid1).andRequireExplicitResponseAuthorization().andThen() if (theRequireExplicitResponseAuthorization) {
.allow().read().resourcesOfType(Patient.class).inCompartment("Patient", pid1).andThen() ruleBuilder.allow().operation().named(JpaConstants.OPERATION_EVERYTHING).onInstance(pid1).andRequireExplicitResponseAuthorization().andThen();
.allow().read().resourcesOfType(Observation.class).inCompartment("Patient", pid1).andThen() } else {
.allow().create().resourcesOfType(Encounter.class).withAnyId().andThen() ruleBuilder.allow().operation().named(JpaConstants.OPERATION_EVERYTHING).onInstance(pid1).andAllowAllResponsesWithAllResourcesAccess().andThen();
.build(); }
ruleBuilder.allow().read().resourcesOfType(Patient.class).inCompartment("Patient", pid1).andThen();
ruleBuilder.allow().read().resourcesOfType(Observation.class).inCompartment("Patient", pid1).andThen();
return ruleBuilder.build();
} }
}); });
Bundle outcome = myClient IOperationUntypedWithInput<Bundle> executable = myClient
.operation() .operation()
.onInstance(pid1) .onInstance(pid1)
.named(JpaConstants.OPERATION_EVERYTHING) .named(JpaConstants.OPERATION_EVERYTHING)
.withNoParameters(Parameters.class) .withNoParameters(Parameters.class)
.returnResourceType(Bundle.class) .returnResourceType(Bundle.class);
.execute();
assertThat(outcome.getEntry()).hasSize(2);
// Add an Encounter, which will be returned by $everything but that hasn't been if (theShouldSucceed) {
// explicitly authorized Bundle outcome = executable.execute();
assertThat(outcome.getEntry()).hasSize(expectedCount);
Encounter enc = new Encounter(); } else {
enc.setSubject(new Reference(pid1)); try {
myClient.create().resource(enc).execute(); executable.execute();
fail();
try { } catch (ForbiddenOperationException e) {
myClient assertThat(e.getMessage()).contains("Access denied by default policy");
.operation() }
.onInstance(pid1)
.named(JpaConstants.OPERATION_EVERYTHING)
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
fail();
} catch (ForbiddenOperationException e) {
assertThat(e.getMessage()).contains("Access denied by default policy");
} }
} }
@ -2029,6 +2142,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
super(theResourceTypes); super(theResourceTypes);
} }
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
List<IAuthRule> rules = new ArrayList<>(super.buildRuleList(theRequestDetails)); List<IAuthRule> rules = new ArrayList<>(super.buildRuleList(theRequestDetails));
List<IAuthRule> rulesToAdd = new RuleBuilder().allow().transaction().withAnyOperation().andApplyNormalRules().build(); List<IAuthRule> rulesToAdd = new RuleBuilder().allow().transaction().withAnyOperation().andApplyNormalRules().build();

View File

@ -79,25 +79,11 @@
<dependency> <dependency>
<groupId>org.simplejavamail</groupId> <groupId>org.simplejavamail</groupId>
<artifactId>simple-java-mail</artifactId> <artifactId>simple-java-mail</artifactId>
<!-- Excluded in favor of jakarta.activation:jakarta.activation-api -->
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.icegreen</groupId> <groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId> <artifactId>greenmail</artifactId>
<scope>compile</scope> <scope>compile</scope>
<!-- Excluded in favor of jakarta.activation:jakarta.activation-api -->
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -164,7 +164,8 @@ public class AuthorizationInterceptor implements IRuleApplier {
rules.size(), rules.size(),
getPointcutNameOrEmpty(thePointcut), getPointcutNameOrEmpty(thePointcut),
getResourceTypeOrEmpty(theInputResource), getResourceTypeOrEmpty(theInputResource),
getResourceTypeOrEmpty(theOutputResource)); getResourceTypeOrEmpty(theOutputResource),
thePointcut);
Verdict verdict = null; Verdict verdict = null;
for (IAuthRule nextRule : rules) { for (IAuthRule nextRule : rules) {
@ -528,7 +529,7 @@ public class AuthorizationInterceptor implements IRuleApplier {
case EXTENDED_OPERATION_TYPE: case EXTENDED_OPERATION_TYPE:
case EXTENDED_OPERATION_INSTANCE: { case EXTENDED_OPERATION_INSTANCE: {
if (theResponseObject != null) { if (theResponseObject != null) {
resources = toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext); resources = toListOfResourcesAndExcludeContainerUnlessStandalone(theResponseObject, fhirContext);
} }
break; break;
} }
@ -575,7 +576,7 @@ public class AuthorizationInterceptor implements IRuleApplier {
OUT, OUT,
} }
protected static List<IBaseResource> toListOfResourcesAndExcludeContainer( protected static List<IBaseResource> toListOfResourcesAndExcludeContainerUnlessStandalone(
IBaseResource theResponseObject, FhirContext fhirContext) { IBaseResource theResponseObject, FhirContext fhirContext) {
if (theResponseObject == null) { if (theResponseObject == null) {
return Collections.emptyList(); return Collections.emptyList();
@ -588,6 +589,13 @@ public class AuthorizationInterceptor implements IRuleApplier {
return Collections.singletonList(theResponseObject); return Collections.singletonList(theResponseObject);
} }
return toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext);
}
@Nonnull
public static List<IBaseResource> toListOfResourcesAndExcludeContainer(
IBaseResource theResponseObject, FhirContext fhirContext) {
List<IBaseResource> retVal;
retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class); retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class);
// Exclude the container // Exclude the container

View File

@ -27,11 +27,9 @@ public interface IAuthRuleBuilderOperationNamedAndScoped {
IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponses(); IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponses();
/** /**
* Responses for this operation will not be checked and access to all resources is allowed. This * @deprecated This is a synonym for {@link #andAllowAllResponses()}, use that method instead
* is intended for operations which are known to fetch a graph of resources that is known to be
* safe, such as `$everything` which may access and fetch resources outside the patient's compartment
* but enforces safety in what it fetches via strict SQL queries.
*/ */
@Deprecated(since = "7.6.0")
IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponsesWithAllResourcesAccess(); IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponsesWithAllResourcesAccess();
/** /**

View File

@ -85,6 +85,7 @@ class OperationRule extends BaseRule implements IAuthRule {
myAppliesToTypes = theAppliesToTypes; myAppliesToTypes = theAppliesToTypes;
} }
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
@Override @Override
public Verdict applyRule( public Verdict applyRule(
RestOperationTypeEnum theOperation, RestOperationTypeEnum theOperation,
@ -97,23 +98,8 @@ class OperationRule extends BaseRule implements IAuthRule {
Pointcut thePointcut) { Pointcut thePointcut) {
FhirContext ctx = theRequestDetails.getServer().getFhirContext(); FhirContext ctx = theRequestDetails.getServer().getFhirContext();
// Operation rules apply to the execution of the operation itself, not to side effects like
// loading resources (that will presumably be reflected in the response). Those loads need
// to be explicitly authorized
if (!myAllowAllResourcesAccess && isResourceAccess(thePointcut)) {
return null;
}
boolean applies = false; boolean applies = false;
switch (theOperation) { switch (theOperation) {
case ADD_TAGS:
case DELETE_TAGS:
case GET_TAGS:
case GET_PAGE:
case GRAPHQL_REQUEST:
// These things can't be tracked by the AuthorizationInterceptor
// at this time
return null;
case EXTENDED_OPERATION_SERVER: case EXTENDED_OPERATION_SERVER:
if (myAppliesToServer || myAppliesAtAnyLevel) { if (myAppliesToServer || myAppliesAtAnyLevel) {
applies = true; applies = true;
@ -138,10 +124,7 @@ class OperationRule extends BaseRule implements IAuthRule {
applies = true; applies = true;
} else { } else {
IIdType requestResourceId = null; IIdType requestResourceId = null;
if (theInputResourceId != null) { if (theRequestDetails.getId() != null) {
requestResourceId = theInputResourceId;
}
if (requestResourceId == null && myAllowAllResponses) {
requestResourceId = theRequestDetails.getId(); requestResourceId = theRequestDetails.getId();
} }
if (requestResourceId != null) { if (requestResourceId != null) {
@ -168,40 +151,6 @@ class OperationRule extends BaseRule implements IAuthRule {
} }
} }
break; break;
case CREATE:
break;
case DELETE:
break;
case HISTORY_INSTANCE:
break;
case HISTORY_SYSTEM:
break;
case HISTORY_TYPE:
break;
case READ:
break;
case SEARCH_SYSTEM:
break;
case SEARCH_TYPE:
break;
case TRANSACTION:
break;
case UPDATE:
break;
case VALIDATE:
break;
case VREAD:
break;
case METADATA:
break;
case META_ADD:
break;
case META:
break;
case META_DELETE:
break;
case PATCH:
break;
default: default:
return null; return null;
} }
@ -214,13 +163,33 @@ class OperationRule extends BaseRule implements IAuthRule {
return null; return null;
} }
return newVerdict( if (theOutputResource == null) {
theOperation, // This is the request part
theRequestDetails, return newVerdict(
theInputResource, theOperation,
theInputResourceId, theRequestDetails,
theOutputResource, theInputResource,
theRuleApplier); theInputResourceId,
theOutputResource,
theRuleApplier);
} else {
// This is the response part, so we might want to check all of the
// resources in the response
if (myAllowAllResponses) {
return newVerdict(
theOperation,
theRequestDetails,
theInputResource,
theInputResourceId,
theOutputResource,
theRuleApplier);
} else {
List<IBaseResource> outputResources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(
theOutputResource, theRequestDetails.getFhirContext());
return RuleImplOp.applyRulesToResponseResources(
theRequestDetails, theRuleApplier, thePointcut, outputResources);
}
}
} }
/** /**

View File

@ -828,31 +828,48 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
return verdict; return verdict;
} else if (theOutputResource != null) { } else if (theOutputResource != null) {
return applyRulesToResponseBundle(theRequestDetails, theOutputResource, theRuleApplier, thePointcut);
List<IBaseResource> outputResources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(
theOutputResource, theRequestDetails.getFhirContext());
Verdict verdict = null;
for (IBaseResource nextResource : outputResources) {
if (nextResource == null) {
continue;
}
Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(
RestOperationTypeEnum.READ, theRequestDetails, null, null, nextResource, thePointcut);
if (newVerdict == null) {
continue;
} else if (verdict == null) {
verdict = newVerdict;
} else if (verdict.getDecision() == PolicyEnum.ALLOW && newVerdict.getDecision() == PolicyEnum.DENY) {
verdict = newVerdict;
}
}
return verdict;
} else { } else {
return null; return null;
} }
} }
@Nullable
private static Verdict applyRulesToResponseBundle(
RequestDetails theRequestDetails,
IBaseResource theOutputResource,
IRuleApplier theRuleApplier,
Pointcut thePointcut) {
List<IBaseResource> outputResources =
AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(
theOutputResource, theRequestDetails.getFhirContext());
return applyRulesToResponseResources(theRequestDetails, theRuleApplier, thePointcut, outputResources);
}
@Nullable
public static Verdict applyRulesToResponseResources(
RequestDetails theRequestDetails,
IRuleApplier theRuleApplier,
Pointcut thePointcut,
List<IBaseResource> outputResources) {
Verdict verdict = null;
for (IBaseResource nextResource : outputResources) {
if (nextResource == null) {
continue;
}
Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(
RestOperationTypeEnum.READ, theRequestDetails, null, null, nextResource, thePointcut);
if (newVerdict == null) {
continue;
} else if (verdict == null) {
verdict = newVerdict;
} else if (verdict.getDecision() == PolicyEnum.ALLOW && newVerdict.getDecision() == PolicyEnum.DENY) {
verdict = newVerdict;
}
}
return verdict;
}
private boolean isInvalidNestedBundleRequest(BundleEntryParts theEntry) { private boolean isInvalidNestedBundleRequest(BundleEntryParts theEntry) {
IBaseResource resource = theEntry.getResource(); IBaseResource resource = theEntry.getResource();
if (!(resource instanceof IBaseBundle)) { if (!(resource instanceof IBaseBundle)) {

View File

@ -21,9 +21,9 @@ package ca.uhn.fhir.rest.server.mail;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import org.simplejavamail.api.email.Email; import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.mailer.AsyncResponse;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
public interface IMailSvc { public interface IMailSvc {
void sendMail(@Nonnull List<Email> theEmails); void sendMail(@Nonnull List<Email> theEmails);
@ -31,7 +31,5 @@ public interface IMailSvc {
void sendMail(@Nonnull Email theEmail); void sendMail(@Nonnull Email theEmail);
void sendMail( void sendMail(
@Nonnull Email theEmail, @Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull Consumer<Throwable> theErrorHandler);
@Nonnull Runnable theOnSuccess,
@Nonnull AsyncResponse.ExceptionConsumer theErrorHandler);
} }

View File

@ -20,12 +20,9 @@
package ca.uhn.fhir.rest.server.mail; package ca.uhn.fhir.rest.server.mail;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import org.apache.commons.lang3.Validate;
import org.simplejavamail.MailException; import org.simplejavamail.MailException;
import org.simplejavamail.api.email.Email; import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.email.Recipient; import org.simplejavamail.api.email.Recipient;
import org.simplejavamail.api.mailer.AsyncResponse;
import org.simplejavamail.api.mailer.AsyncResponse.ExceptionConsumer;
import org.simplejavamail.api.mailer.Mailer; import org.simplejavamail.api.mailer.Mailer;
import org.simplejavamail.api.mailer.config.TransportStrategy; import org.simplejavamail.api.mailer.config.TransportStrategy;
import org.simplejavamail.mailer.MailerBuilder; import org.simplejavamail.mailer.MailerBuilder;
@ -33,6 +30,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class MailSvc implements IMailSvc { public class MailSvc implements IMailSvc {
@ -42,14 +41,14 @@ public class MailSvc implements IMailSvc {
private final Mailer myMailer; private final Mailer myMailer;
public MailSvc(@Nonnull MailConfig theMailConfig) { public MailSvc(@Nonnull MailConfig theMailConfig) {
Validate.notNull(theMailConfig); Objects.requireNonNull(theMailConfig);
myMailConfig = theMailConfig; myMailConfig = theMailConfig;
myMailer = makeMailer(myMailConfig); myMailer = makeMailer(myMailConfig);
} }
@Override @Override
public void sendMail(@Nonnull List<Email> theEmails) { public void sendMail(@Nonnull List<Email> theEmails) {
Validate.notNull(theEmails); Objects.requireNonNull(theEmails);
theEmails.forEach(theEmail -> send(theEmail, new OnSuccess(theEmail), new ErrorHandler(theEmail))); theEmails.forEach(theEmail -> send(theEmail, new OnSuccess(theEmail), new ErrorHandler(theEmail)));
} }
@ -60,21 +59,23 @@ public class MailSvc implements IMailSvc {
@Override @Override
public void sendMail( public void sendMail(
@Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull ExceptionConsumer theErrorHandler) { @Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull Consumer<Throwable> theErrorHandler) {
send(theEmail, theOnSuccess, theErrorHandler); send(theEmail, theOnSuccess, theErrorHandler);
} }
private void send( private void send(
@Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull ExceptionConsumer theErrorHandler) { @Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull Consumer<Throwable> theErrorHandler) {
Validate.notNull(theEmail); Objects.requireNonNull(theEmail);
Validate.notNull(theOnSuccess); Objects.requireNonNull(theOnSuccess);
Validate.notNull(theErrorHandler); Objects.requireNonNull(theErrorHandler);
try { try {
final AsyncResponse asyncResponse = myMailer.sendMail(theEmail, true); myMailer.sendMail(theEmail, true).whenComplete((result, ex) -> {
if (asyncResponse != null) { if (ex != null) {
asyncResponse.onSuccess(theOnSuccess); theErrorHandler.accept(ex);
asyncResponse.onException(theErrorHandler); } else {
} theOnSuccess.run();
}
});
} catch (MailException e) { } catch (MailException e) {
theErrorHandler.accept(e); theErrorHandler.accept(e);
} }
@ -117,7 +118,7 @@ public class MailSvc implements IMailSvc {
} }
} }
private class ErrorHandler implements ExceptionConsumer { private class ErrorHandler implements Consumer<Throwable> {
private final Email myEmail; private final Email myEmail;
private ErrorHandler(@Nonnull Email theEmail) { private ErrorHandler(@Nonnull Email theEmail) {
@ -125,7 +126,7 @@ public class MailSvc implements IMailSvc {
} }
@Override @Override
public void accept(Exception t) { public void accept(Throwable t) {
ourLog.error("Email not sent" + makeMessage(myEmail), t); ourLog.error("Email not sent" + makeMessage(myEmail), t);
} }
} }

View File

@ -4,6 +4,7 @@ import com.icegreen.greenmail.junit5.GreenMailExtension;
import com.icegreen.greenmail.util.GreenMailUtil; import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetupTest; import com.icegreen.greenmail.util.ServerSetupTest;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import jakarta.mail.internet.MimeMessage;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
@ -11,7 +12,6 @@ import org.simplejavamail.MailException;
import org.simplejavamail.api.email.Email; import org.simplejavamail.api.email.Email;
import org.simplejavamail.email.EmailBuilder; import org.simplejavamail.email.EmailBuilder;
import javax.mail.internet.MimeMessage;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -86,13 +86,14 @@ public class MailSvcIT {
@Test @Test
public void testSendMailWithInvalidToAddressExpectErrorHandler() { public void testSendMailWithInvalidToAddressExpectErrorHandler() {
// setup // setup
final Email email = withEmail("xyz"); String invalidEmailAdress = "xyz";
final Email email = withEmail(invalidEmailAdress);
// execute // execute
fixture.sendMail(email, fixture.sendMail(email,
() -> fail("Should not execute on Success"), () -> fail("Should not execute on Success"),
(e) -> { (e) -> {
assertTrue(e instanceof MailException); assertTrue(e instanceof MailException);
assertEquals("Invalid TO address: " + email, e.getMessage()); assertEquals("Invalid TO address: " + invalidEmailAdress, e.getMessage());
}); });
// validate // validate
assertTrue(ourGreenMail.waitForIncomingEmail(1000, 0)); assertTrue(ourGreenMail.waitForIncomingEmail(1000, 0));

View File

@ -4230,7 +4230,7 @@ public class AuthorizationInterceptorR4Test extends BaseValidationTestWithInline
RequestDetails requestDetails = new SystemRequestDetails(); RequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setResourceName("Bundle"); requestDetails.setResourceName("Bundle");
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(searchSet, ourCtx); List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(searchSet, ourCtx);
assertEquals(1, resources.size()); assertEquals(1, resources.size());
assertTrue(resources.contains(bundle)); assertTrue(resources.contains(bundle));
} }
@ -4247,7 +4247,7 @@ public class AuthorizationInterceptorR4Test extends BaseValidationTestWithInline
RequestDetails requestDetails = new SystemRequestDetails(); RequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setResourceName("Patient"); requestDetails.setResourceName("Patient");
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(searchSet, ourCtx); List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainerUnlessStandalone(searchSet, ourCtx);
assertEquals(2, resources.size()); assertEquals(2, resources.size());
assertTrue(resources.contains(patient1)); assertTrue(resources.contains(patient1));
assertTrue(resources.contains(patient2)); assertTrue(resources.contains(patient2));

34
pom.xml
View File

@ -869,6 +869,7 @@
<developer> <developer>
<id>delopst</id> <id>delopst</id>
<name>Primož Delopst</name> <name>Primož Delopst</name>
<organization>Better</organization>
</developer> </developer>
<developer> <developer>
<id>Zach Smith</id> <id>Zach Smith</id>
@ -1160,27 +1161,38 @@
<dependency> <dependency>
<groupId>org.simplejavamail</groupId> <groupId>org.simplejavamail</groupId>
<artifactId>simple-java-mail</artifactId> <artifactId>simple-java-mail</artifactId>
<version>6.6.1</version> <version>8.11.2</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>com.sun.activation</groupId> <groupId>com.github.bbottema</groupId>
<artifactId>jakarta.activation-api</artifactId> <artifactId>jetbrains-runtime-annotations</artifactId>
</exclusion> </exclusion>
<exclusion> <exclusion>
<groupId>com.sun.activation</groupId> <groupId>jakarta.mail</groupId>
<artifactId>jakarta.activation</artifactId> <artifactId>jakarta.mail-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>2.1.0-rc-1</version>
<exclusions>
<exclusion>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>1.6.4</version>
</dependency>
<dependency> <dependency>
<groupId>com.icegreen</groupId> <groupId>com.icegreen</groupId>
<artifactId>greenmail-junit5</artifactId> <artifactId>greenmail-junit5</artifactId>
<version>1.6.4</version> <version>2.1.0-rc-1</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- mail end --> <!-- mail end -->