Skip to content

Commit

Permalink
Merge pull request #306 from medizininformatik-initiative/release/v5.0.0
Browse files Browse the repository at this point in the history
Release v5.0.0
  • Loading branch information
michael-82 authored Jun 26, 2024
2 parents f2d5d19 + 7bc8d89 commit 491a25c
Show file tree
Hide file tree
Showing 13 changed files with 4,546 additions and 45 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [5.0.0] - 2024-06-26

### Added
- Added an endpoint to validate uploaded structured queries. ([#258](https://github.com/medizininformatik-initiative/feasibility-backend/issues/258))
- OpenID Connect authentication for direct broker ([#302](https://github.com/medizininformatik-initiative/feasibility-backend/issues/302))
### Changed
- Validation for structured queries has been reworked. ([#260](https://github.com/medizininformatik-initiative/feasibility-backend/issues/260)), ([#266](https://github.com/medizininformatik-initiative/feasibility-backend/issues/266))
- Updated sq2cql to v0.3.0
- Updated ontology to version v2.2.0 ([#299](https://github.com/medizininformatik-initiative/feasibility-backend/issues/299))
### Fixed
- Increased timeout in MockBrockerClientIT to avoid occasional test failures ([#276](https://github.com/medizininformatik-initiative/feasibility-backend/issues/276))
- OPS codes with lowercase letters are now correctly found ([#292](https://github.com/medizininformatik-initiative/feasibility-backend/issues/292))
### Security
- updated spring boot to 3.3.1
- updated undertow to 2.3.14.Final to fix [CVE-2024-6162](https://avd.aquasec.com/nvd/2024/cve-2024-6162/) ([#304](https://github.com/medizininformatik-initiative/feasibility-backend/issues/304))
- Updated netty-codec-http to 4.1.108.Final to fix [CVE-2024-29025](https://avd.aquasec.com/nvd/cve-2024-29025) ([#279](https://github.com/medizininformatik-initiative/feasibility-backend/issues/279))
- Updated nimbus-jose-jwt to 9.37.3 to fix [CVE-2023-52428](https://avd.aquasec.com/nvd/cve-2023-52428) ([#275](https://github.com/medizininformatik-initiative/feasibility-backend/issues/275))
- Updated xnio to 3.8.14.Final to fix [CVE-2023-5685](https://avd.aquasec.com/nvd/cve-2023-5685) ([#274](https://github.com/medizininformatik-initiative/feasibility-backend/issues/274))

## [5.0.0-rc.1] - 2024-06-17

### Changed
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@ The DIRECT path can be run **either** with FLARE **or** with a CQL compatible se
Result counts from the direct path can be obfuscated for privacy reasons. The current implementation
handles obfuscation by adding or subtracting a random number <=5.

| EnvVar | Description | Example | Default |
|-------------------------------------------|--------------------------------------------------------------------------------|--------------------|---------|
| BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME | Username to use to connect to flare or directly to the FHIR server via CQL | feas-user | |
| BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD | Password for that user | verysecurepassword | |
| BROKER_CLIENT_DIRECT_USE_CQL | Whether to use a CQL server or not. | | false |
| BROKER_CLIENT_OBFUSCATE_RESULT_COUNT | Whether the result counts retrieved from the direct broker shall be obfuscated | | false |
| EnvVar | Description | Example | Default |
|-----------------------------------------------|-------------------------------------------------------------------------------------------------|-------------------------------------|---------|
| BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME | Username to use to connect to flare or directly to the FHIR server via CQL | feas-user | |
| BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD | Password for that user | verysecurepassword | |
| BROKER_CLIENT_DIRECT_AUTH_OAUTH_ISSUER_URL | Issuer URL of OpenID Connect provider for authenticating access to OAuth2 protected FHIR server | https://auth.example.com/realms/foo | |
| BROKER_CLIENT_DIRECT_AUTH_OAUTH_CLIENT_ID | Client ID to use when authenticating at OpenID Connect provider | foo_client | |
| BROKER_CLIENT_DIRECT_AUTH_OAUTH_CLIENT_SECRET | Client secret to use when authenticating at OpenID Connect provider | verysecurepassword | |
| BROKER_CLIENT_DIRECT_USE_CQL | Whether to use a CQL server or not. | | false |
| BROKER_CLIENT_OBFUSCATE_RESULT_COUNT | Whether the result counts retrieved from the direct broker shall be obfuscated | | false |

This is irrelevant if _BROKER_CLIENT_DIRECT_ENABLED_ is set to false.

Expand Down
48 changes: 38 additions & 10 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<version>3.3.1</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>

<groupId>de.medizininformatik-initiative</groupId>
<artifactId>FeasibilityGuiBackend</artifactId>
<version>5.0.0-rc.1</version>
<version>5.0.0</version>

<name>FeasibilityGuiBackend</name>
<description>Backend of the Feasibility GUI</description>
Expand Down Expand Up @@ -52,13 +53,27 @@
<artifactId>xnio-api</artifactId>
<version>3.8.14.Final</version>
</dependency>

<dependency>
<groupId>org.jboss.xnio</groupId>
<artifactId>xnio-nio</artifactId>
<version>3.8.14.Final</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>2.3.14.Final</version>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<version>2.3.14.Final</version>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-websockets-jsr</artifactId>
<version>2.3.14.Final</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down Expand Up @@ -100,7 +115,13 @@
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.3</version>
<version>9.40</version>
</dependency>

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.12</version>
</dependency>

<dependency>
Expand Down Expand Up @@ -224,7 +245,7 @@
<dependency>
<groupId>de.medizininformatik-initiative</groupId>
<artifactId>sq2cql</artifactId>
<version>0.3.0-rc.1</version>
<version>0.3.0</version>
</dependency>

<dependency>
Expand Down Expand Up @@ -262,7 +283,14 @@
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.7</version>
<version>1.19.8</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>3.4.0</version>
<scope>test</scope>
</dependency>

Expand All @@ -277,14 +305,14 @@
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.17.6</version>
<version>1.19.8</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.17.6</version>
<version>1.19.8</version>
<scope>test</scope>
</dependency>

Expand Down Expand Up @@ -371,7 +399,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<version>0.8.12</version>
<executions>
<execution>
<id>prepare-agent</id>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.numcodex.feasibility_gui_backend.query.broker;

public class OAuthClientException extends RuntimeException {

private static final long serialVersionUID = -5840162115734733430L;

public OAuthClientException(String message) {
super(message);
}

public OAuthClientException(String message, Exception cause) {
super(message, cause);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package de.numcodex.feasibility_gui_backend.query.broker;

import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import com.nimbusds.oauth2.sdk.AccessTokenResponse;
import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
import com.nimbusds.oauth2.sdk.GeneralException;
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
import com.nimbusds.oauth2.sdk.TokenRequest;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import lombok.NonNull;
import org.joda.time.DateTime;

import java.io.IOException;
import java.net.URI;

public final class OAuthInterceptor implements IClientInterceptor {

private static final int TOKEN_EXPIRY_THRESHOLD = 5000;
private HTTPRequest tokenRequest;
private AccessToken token;
private DateTime tokenExpiry;
private Issuer issuer;
private ClientSecretBasic clientAuth;

public OAuthInterceptor(@NonNull String oauthIssuerUrl, @NonNull String oauthClientId,
@NonNull String oauthClientSecret) {
clientAuth = new ClientSecretBasic(new ClientID(oauthClientId), new Secret(oauthClientSecret));
issuer = new Issuer(oauthIssuerUrl);
}

public String getToken() {
if (token == null || DateTime.now().plus(TOKEN_EXPIRY_THRESHOLD).isAfter(tokenExpiry)) {
try {
TokenResponse response = TokenResponse.parse(getTokenRequest().send());
if (!response.indicatesSuccess()) {
TokenErrorResponse errorResponse = response.toErrorResponse();
throw new OAuthClientException(errorResponse.getErrorObject().getCode() + " - "
+ errorResponse.getErrorObject().getDescription());
}
AccessTokenResponse successResponse = response.toSuccessResponse();

token = successResponse.getTokens().getAccessToken();
tokenExpiry = DateTime.now().plus(token.getLifetime() * 1000);
} catch (GeneralException | IOException e) {
throw new OAuthClientException("Request for OAuth2 access token failed", e);
}
}
return token.getValue();
}

private HTTPRequest getTokenRequest() throws GeneralException, IOException {
if (tokenRequest == null) {
tokenRequest = new TokenRequest(getTokenUri(), clientAuth, new ClientCredentialsGrant()).toHTTPRequest();
}
return tokenRequest;
}

private URI getTokenUri() throws GeneralException, IOException {
return OIDCProviderMetadata.resolve(issuer).getTokenEndpointURI();
}

@Override
public void interceptRequest(IHttpRequest theRequest) {
theRequest.addHeader(Constants.HEADER_AUTHORIZATION,
Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER + getToken());
}

@Override
public void interceptResponse(IHttpResponse theResponse) throws IOException {
}
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,61 @@
package de.numcodex.feasibility_gui_backend.query.broker.direct;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor;
import de.numcodex.feasibility_gui_backend.query.broker.BrokerClient;
import de.numcodex.feasibility_gui_backend.query.broker.OAuthInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.reactive.function.client.WebClient;

import static com.google.common.base.Strings.isNullOrEmpty;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

/**
* Spring configuration for providing a {@link DirectBrokerClient} implementation instance.
* Either {@link DirectBrokerClientCql} or {@link DirectBrokerClientFlare}
* Spring configuration for providing a {@link DirectBrokerClient} implementation instance. Either
* {@link DirectBrokerClientCql} or {@link DirectBrokerClientFlare}
*/
@Lazy
@Configuration
@Slf4j
public class DirectSpringConfig {

private final boolean useCql;

private final String flareBaseUrl;

private final String cqlBaseUrl;

private final String username;

private final String password;
private String issuer;
private String clientId;
private String clientSecret;

public DirectSpringConfig(@Value("${app.broker.direct.useCql:false}") boolean useCql, @Value("${app.flare.baseUrl}") String flareBaseUrl, @Value("${app.cql.baseUrl}") String cqlBaseUrl, @Value("${app.broker.direct.auth.basic.username}") String username, @Value("${app.broker.direct.auth.basic.password}") String password) {
public DirectSpringConfig(@Value("${app.broker.direct.useCql:false}") boolean useCql,
@Value("${app.flare.baseUrl}") String flareBaseUrl, @Value("${app.cql.baseUrl}") String cqlBaseUrl,
@Value("${app.broker.direct.auth.basic.username}") String username,
@Value("${app.broker.direct.auth.basic.password}") String password,
@Value("${app.broker.direct.auth.oauth.issuer.url}") String issuer,
@Value("${app.broker.direct.auth.oauth.client.id") String clientId,
@Value("${app.broker.direct.auth.oauth.client.secret}") String clientSecret) {
this.useCql = useCql;
this.flareBaseUrl = flareBaseUrl;
this.cqlBaseUrl = cqlBaseUrl;
this.username = username;
this.password = password;
this.issuer = issuer;
this.clientId = clientId;
this.clientSecret = clientSecret;
}

@Qualifier("direct")
@Bean
public BrokerClient directBrokerClient(WebClient directWebClientFlare, @Value("${app.broker.direct.obfuscateResultCount:false}") boolean obfuscateResultCount,
FhirConnector fhirConnector, FhirHelper fhirHelper) {
public BrokerClient directBrokerClient(WebClient directWebClientFlare,
@Value("${app.broker.direct.obfuscateResultCount:false}") boolean obfuscateResultCount,
FhirConnector fhirConnector, FhirHelper fhirHelper) {
if (useCql) {
return new DirectBrokerClientCql(fhirConnector, obfuscateResultCount, fhirHelper);
} else {
Expand All @@ -54,21 +66,31 @@ public BrokerClient directBrokerClient(WebClient directWebClientFlare, @Value("$
@Bean
public IGenericClient getFhirClient(FhirContext fhirContext) {
IGenericClient iGenericClient = fhirContext.newRestfulGenericClient(cqlBaseUrl);
if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) {
IClientInterceptor authInterceptor = new BasicAuthInterceptor(username, password);
iGenericClient.registerInterceptor(authInterceptor);
if (!isNullOrEmpty(password) && !isNullOrEmpty(username)) {
log.info("Enable direct broker with basic authentication (type: cql, url: {}, username: {})", cqlBaseUrl,
username);
iGenericClient.registerInterceptor(new BasicAuthInterceptor(username, password));
} else if (!isNullOrEmpty(issuer) && !isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) {
log.info("Enable direct broker with oauth authentication (type: cql, url: {}, issuer: {}, client-id: {})",
cqlBaseUrl, issuer, clientId);
iGenericClient.registerInterceptor(new OAuthInterceptor(issuer, clientId, clientSecret));
} else {
log.info("Enable direct broker (type: cql, url: {})", cqlBaseUrl);
}
return iGenericClient;
}

@Bean
public WebClient directWebClientFlare() {
if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) {
if (!isNullOrEmpty(password) && !isNullOrEmpty(username)) {
log.info("Enable direct broker with basic authentication (type: flare, url: {}, username: {})",
flareBaseUrl, username);
return WebClient.builder()
.filter(basicAuthentication(username, password))
.baseUrl(flareBaseUrl)
.build();
} else {
log.info("Enable direct broker (type: flare, url: {})", flareBaseUrl);
return WebClient.create(flareBaseUrl);
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ app:
basic:
username: ${BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME:}
password: ${BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD:}
oauth:
issuer:
url: ${BROKER_CLIENT_DIRECT_AUTH_OAUTH_ISSUER_URL:}
client:
id: ${BROKER_CLIENT_DIRECT_AUTH_OAUTH_CLIENT_ID:}
secret: ${BROKER_CLIENT_DIRECT_AUTH_OAUTH_CLIENT_SECRET:}
enabled: ${BROKER_CLIENT_DIRECT_ENABLED:false}
useCql: ${BROKER_CLIENT_DIRECT_USE_CQL:false}
obfuscateResultCount: ${BROKER_CLIENT_OBFUSCATE_RESULT_COUNT:false}
Expand Down
Loading

0 comments on commit 491a25c

Please sign in to comment.