Skip to content

Commit

Permalink
Merge pull request #701 from lantanagroup/LNK-1523
Browse files Browse the repository at this point in the history
LNK-1523: Update measure evaluation to evaluate directly within the API
  • Loading branch information
smailliwcs committed Feb 26, 2024
2 parents 48458a0 + d36e4b1 commit 1253a26
Show file tree
Hide file tree
Showing 43 changed files with 384 additions and 403 deletions.
4 changes: 2 additions & 2 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 9 additions & 30 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,37 +79,26 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>${spring.boot.starter.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.boot.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${spring.boot.version}</version>
</dependency>

<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
<version>${hapi.lib.version}</version>
</dependency>

<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>${saxon.version}</version>
</dependency>

<dependency>
Expand All @@ -124,18 +113,6 @@
<version>0.22.1</version>
</dependency>

<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
</dependency>

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>7.9</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
Expand All @@ -144,16 +121,9 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.5.2</version>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand All @@ -163,6 +133,15 @@
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

<dependency>
<groupId>org.opencds.cqf.fhir</groupId>
<artifactId>cqf-fhir-cr</artifactId>
</dependency>
<dependency>
<groupId>org.opencds.cqf.fhir</groupId>
<artifactId>cqf-fhir-jackson</artifactId>
<type>pom</type>
</dependency>
</dependencies>

<build>
Expand Down
38 changes: 2 additions & 36 deletions api/src/main/java/com/lantanagroup/link/api/ApiInit.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@

import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.lantanagroup.link.FhirContextProvider;
import com.lantanagroup.link.FhirDataProvider;
import com.lantanagroup.link.config.api.ApiConfig;
import com.lantanagroup.link.db.SharedService;
import com.lantanagroup.link.db.TenantService;
import com.lantanagroup.link.db.model.tenant.FhirQuery;
import com.lantanagroup.link.db.model.tenant.Tenant;
import com.lantanagroup.link.validation.Validator;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.Parameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -32,15 +29,12 @@ public class ApiInit {
@Autowired
private SharedService sharedService;

@Autowired
private Validator validator;

private boolean checkPrerequisites() {
logger.info("Checking that API prerequisite services are available. maxRetry: {}, retryWait: {}", config.getMaxRetry(), config.getRetryWait());

boolean allServicesAvailable = false;
boolean terminologyServiceAvailable = false;
boolean evaluationServiceAvailable = false;
boolean terminologyServiceAvailable = StringUtils.isEmpty(config.getTerminologyService());
boolean evaluationServiceAvailable = true; // In-process evaluation is always "available"

for (int retry = 0; config.getMaxRetry() == null || retry <= config.getMaxRetry(); retry++) {
// Check terminology service availability
Expand All @@ -53,17 +47,6 @@ private boolean checkPrerequisites() {
}
}

// Check evaluation service availability
if (!evaluationServiceAvailable) {
try {
new FhirDataProvider(config.getEvaluationService()).getClient().capabilities().ofType(CapabilityStatement.class).execute();
evaluationServiceAvailable = true;
} catch (BaseServerResponseException e) {
logger.error(String.format("Could not connect to evaluation service %s (%s)", config.getEvaluationService(), e));
}
}


// Check if all services are now available
allServicesAvailable = terminologyServiceAvailable && evaluationServiceAvailable;
if (allServicesAvailable) {
Expand Down Expand Up @@ -122,7 +105,6 @@ public void init() {
this.sharedService.initDatabase();
}

this.validator.init();
List<Tenant> tenants = this.sharedService.getTenantConfigs();

for (Tenant tenant : tenants) {
Expand All @@ -149,21 +131,5 @@ public void init() {
if (!this.checkPrerequisites()) {
throw new IllegalStateException("Prerequisite services check failed. Cannot continue API initialization.");
}

ensureSupplementalDataSearchParameter();
}

private void ensureSupplementalDataSearchParameter() {
logger.info("Requesting evaluation of nonexistent measure to ensure supplemental-data search parameter exists");
try {
FhirContextProvider.getFhirContext().newRestfulGenericClient(this.config.getEvaluationService())
.operation()
.onInstance("Measure/nonexistent-measure")
.named("$evaluate-measure")
.withNoParameters(Parameters.class)
.execute();
} catch (ResourceNotFoundException e) {
logger.info("Caught 404 as expected");
}
}
}
81 changes: 29 additions & 52 deletions api/src/main/java/com/lantanagroup/link/api/ApiSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
import com.lantanagroup.link.config.api.ApiConfig;
import com.lantanagroup.link.db.SharedService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;

import java.util.List;
Expand All @@ -25,7 +24,7 @@
@Configuration
@EnableWebSecurity
@Order(1)
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
public class ApiSecurityConfig {
@Autowired
private ApiConfig config;

Expand All @@ -35,57 +34,35 @@ public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
// @Autowired
// private CsrfTokenRepository customCsrfTokenRepository; //custom csrfToken repository

@Override
protected void configure(HttpSecurity http) throws Exception {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
PreAuthTokenHeaderFilter authFilter = new PreAuthTokenHeaderFilter("Authorization", config, this.sharedService);
authFilter.setAuthenticationManager(new AuthenticationManager() {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
LinkCredentials credentials = (LinkCredentials) authentication.getPrincipal();

if (credentials.getUser() != null) {
authentication.setAuthenticated(true);
}

return authentication;
authFilter.setAuthenticationManager(authentication -> {
LinkCredentials credentials = (LinkCredentials) authentication.getPrincipal();
if (credentials.getUser() != null) {
authentication.setAuthenticated(true);
}
return authentication;
});

http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
// .csrf().csrfTokenRepository(customCsrfTokenRepository) //.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// .and()
//.cors()
.cors().configurationSource(request -> {
return http.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(AbstractHttpConfigurer::disable)
.cors(configurer -> configurer.configurationSource(request -> {
var cors = new CorsConfiguration();
cors.setAllowedOrigins(List.of(config.getCors().getAllowedOrigins()));
cors.setAllowedMethods(List.of(config.getCors().getAllowedMethods()));
cors.setAllowedHeaders(List.of(config.getCors().getAllowedHeaders()));
cors.setAllowCredentials(config.getCors().getAllowedCredentials());
return cors;
})
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.antMatchers("/config/**", "/api", "/api/docs")
.permitAll()
.and()
.antMatcher("/api/**")
cors.setAllowedOrigins(List.of(config.getCors().getAllowedOrigins()));
cors.setAllowedMethods(List.of(config.getCors().getAllowedMethods()));
cors.setAllowedHeaders(List.of(config.getCors().getAllowedHeaders()));
cors.setAllowCredentials(config.getCors().getAllowedCredentials());
return cors;
}))
.addFilter(authFilter)
.authorizeRequests()
.anyRequest()
.authenticated();

//set content security policy
String csp = "script-src 'self'";
http.headers().contentSecurityPolicy(csp);

.authorizeHttpRequests(registry -> {
registry.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll();
registry.requestMatchers("/config/**", "/api", "/api/docs").permitAll();
registry.requestMatchers("/api/**").authenticated();
})
.headers(configurer -> configurer.contentSecurityPolicy(csp -> {
csp.policyDirectives("script-src 'self'");
}))
.build();
}

}


39 changes: 39 additions & 0 deletions api/src/main/java/com/lantanagroup/link/api/MeasureDef.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.lantanagroup.link.api;

import com.lantanagroup.link.StreamUtils;
import lombok.Getter;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Measure;

import java.util.List;
import java.util.stream.Collectors;

@Getter
public class MeasureDef {
private final Measure measure;
private final List<IBaseResource> resources;

public MeasureDef(Bundle bundle) {
measure = bundle.getEntry().stream()
.map(Bundle.BundleEntryComponent::getResource)
.filter(resource -> resource instanceof Measure)
.map(resource -> (Measure) resource)
.reduce(StreamUtils::toOnlyElement)
.orElseThrow();

// Ensure all populations have an ID
for (Measure.MeasureGroupComponent group : measure.getGroup()) {
for (Measure.MeasureGroupPopulationComponent population : group.getPopulation()) {
if (!population.hasId()) {
population.setId(population.getCode().getCodingFirstRep().getCode());
}
}
}

resources = bundle.getEntry().stream()
.map(Bundle.BundleEntryComponent::getResource)
.filter(resource -> !(resource instanceof Measure))
.collect(Collectors.toList());
}
}
Loading

0 comments on commit 1253a26

Please sign in to comment.