Skip to content

Commit

Permalink
Merge pull request #256 from medizininformatik-initiative/release/4.3.0
Browse files Browse the repository at this point in the history
Release 4.3.0
  • Loading branch information
michael-82 authored Feb 2, 2024
2 parents 3276354 + 55170eb commit 7487cbc
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 31 deletions.
11 changes: 5 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ 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/),

## [UNRELEASED] - yyyy-mm-dd
## [4.3.0] - 2024-02-02

### Added
- Basic auth for direct broker ([#210](https://github.com/medizininformatik-initiative/feasibility-backend/issues/210))
### Changed
### Deprecated
### Removed
### Fixed
- Updated sq2cql to 0.2.14 ([#253](https://github.com/medizininformatik-initiative/feasibility-backend/issues/253))
- Reduce verbosity of DSF Webservice client ([#247](https://github.com/medizininformatik-initiative/feasibility-backend/issues/247))
### Security

The full changelog can be found [here](https://todo).
- Updated spring boot to 3.2.2 ([#251](https://github.com/medizininformatik-initiative/feasibility-backend/issues/251))

## [4.2.0] - 2023-11-17

Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
| QUERY_VALIDATION_ENABLED | When enabled, any structured query submitted via the `run-query` endpoint is validated against the JSON schema located in `src/main/resources/query/query-schema.json` | true / false | true |
| QUERYRESULT_EXPIRY_MINUTES | How many minutes should query results be kept in memory? | | 5 |
| QUERYRESULT_PUBLIC_KEY | The public key in Base64-encoded DER format without banners and line breaks. Mandatory if _QUERYRESULT_DISABLE_LOG_FILE_ENCRYPTION_ is _false_ |
| QUERYRESULT_DISABLE_LOG_FILE_ENCRYPTION | Disable encryption of the result log file. | true / false | |
| QUERYRESULT_DISABLE_LOG_FILE_ENCRYPTION | Disable encryption of the result log file. | true / false | |
| ALLOWED_ORIGINS | Allowed origins for cross-origin requests. This should at least cover the frontend address. | | http://localhost |
| MAX_SAVED_QUERIES_PER_USER | How many slots does a user have to store saved queries. | | 10 |

Expand All @@ -45,10 +45,12 @@ 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_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_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 Expand Up @@ -92,10 +94,10 @@ In order to run the backend using the DSF path, the following environment variab
| DSF_PROXY_USERNAME | Proxy username to be used. | | |
| DSF_PROXY_PASSWORD | Proxy password to be used. | | |
| DSF_WEBSERVICE_BASE_URL | Base URL pointing to the local ZARS FHIR server. | `https://zars/fhir` | |
| DSF_WEBSERVICE_LOG_REQUESTS | Log webservice client communication at log level INFO or below (**WARNING**: potentially contains sensitive data) | `true` | `false` |
| DSF_WEBSOCKET_URL | URL pointing to the local ZARS FHIR server websocket endpoint. | `wss://zars/fhir/ws` | |
| DSF_ORGANIZATION_ID | Identifier for the local organization this backend is part of. | `MY ZARS` | |


### Privacy and Obfuscation

In order to prevent potentially malicious attempts to obtain critical patient data, several
Expand Down
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ services:
QUERYRESULT_EXPIRY_MINUTES: ${CODEX_FEASIBILITY_BACKEND_QUERYRESULT_EXPIRY_MINUTES:-5}
MAX_SAVED_QUERIES_PER_USER: ${CODEX_FEASIBILITY_BACKEND_MAX_SAVED_QUERIES_PER_USER:-10}
# ---- Direct
BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME: ${CODEX_FEASIBILITY_BACKEND_BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME}
BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD: ${CODEX_FEASIBILITY_BACKEND_BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD}
BROKER_CLIENT_DIRECT_USE_CQL: ${CODEX_FEASIBILITY_BACKEND_BROKER_CLIENT_DIRECT_USE_CQL:-false}
BROKER_CLIENT_OBFUSCATE_RESULT_COUNT: ${CODEX_FEASIBILITY_BACKEND_BROKER_CLIENT_OBFUSCATE_RESULT_COUNT:-false}
# ---- Aktin
Expand Down Expand Up @@ -65,6 +67,7 @@ services:
PRIVACY_THRESHOLD_RESULTS: ${CODEX_FEASIBILITY_BACKEND_PRIVACY_THRESHOLD_RESULTS}
PRIVACY_THRESHOLD_SITES: ${CODEX_FEASIBILITY_BACKEND_PRIVACY_THRESHOLD_SITES}
PRIVACY_THRESHOLD_SITES_RESULT: ${CODEX_FEASIBILITY_BACKEND_PRIVACY_THRESHOLD_SITES_RESULT}
QUERYRESULT_DISABLE_LOG_FILE_ENCRYPTION: "false"
volumes:
- ${CODEX_FEASIBILITY_BACKEND_LOCAL_CONCEPT_TREE_PATH:-./ontology/codex-code-tree.json}:${CODEX_FEASIBILITY_BACKEND_ONTOLOGY_FILES_FOLDER:-/opt/codex-feasibility-backend/ontology}/codex-code-tree.json
- ${CODEX_FEASIBILITY_BACKEND_LOCAL_TERM_CODE_MAPPING_PATH:-./ontology/codex-term-code-mapping.json}:${CODEX_FEASIBILITY_BACKEND_ONTOLOGY_FILES_FOLDER:-/opt/codex-feasibility-backend/ontology}/codex-term-code-mapping.json
Expand All @@ -79,3 +82,11 @@ services:
- POSTGRES_USER=codex-postgres
- POSTGRES_PASSWORD=codex-password
- POSTGRES_DB=codex_ui
volumes:
- type: volume
source: feas-backend-db-data
target: /var/lib/postgresql/data

volumes:
feas-backend-db-data:
name: "feas-backend-db-data"
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<version>3.2.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>

<groupId>de.medizininformatik-initiative</groupId>
<artifactId>FeasibilityGuiBackend</artifactId>
<version>4.2.0</version>
<version>4.3.0</version>

<name>FeasibilityGuiBackend</name>
<description>Backend of the Feasibility GUI</description>
Expand Down Expand Up @@ -198,7 +198,7 @@
<dependency>
<groupId>de.medizininformatik-initiative</groupId>
<artifactId>sq2cql</artifactId>
<version>0.2.4</version>
<version>0.2.14</version>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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 org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -10,6 +12,8 @@
import org.springframework.context.annotation.Lazy;
import org.springframework.web.reactive.function.client.WebClient;

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}
Expand All @@ -18,36 +22,55 @@
@Configuration
public class DirectSpringConfig {

@Value("${app.broker.direct.useCql:false}")
private boolean useCql;
private final boolean useCql;

private final String flareBaseUrl;

private final String cqlBaseUrl;

@Value("${app.flare.baseUrl}")
private String flareBaseUrl;
private final String username;

@Value("${app.cql.baseUrl}")
private String cqlBaseUrl;
private final 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) {
this.useCql = useCql;
this.flareBaseUrl = flareBaseUrl;
this.cqlBaseUrl = cqlBaseUrl;
this.username = username;
this.password = password;
}

@Qualifier("direct")
@Bean
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);
return new DirectBrokerClientCql(fhirConnector, obfuscateResultCount, fhirHelper);
} else {
return new DirectBrokerClientFlare(directWebClientFlare, obfuscateResultCount);
}
}

@Bean
public IGenericClient getFhirClient(FhirContext fhirContext){
return fhirContext.newRestfulGenericClient(cqlBaseUrl);
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);
}
return iGenericClient;
}

@Bean
public WebClient directWebClientFlare() {
return WebClient.create(flareBaseUrl);
if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) {
return WebClient.builder()
.filter(basicAuthentication(username, password))
.baseUrl(flareBaseUrl)
.build();
} else {
return WebClient.create(flareBaseUrl);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,22 @@ class DSFFhirWebClientProvider implements FhirWebClientProvider {
private final FhirSecurityContextProvider securityContextProvider;
private FhirSecurityContext securityContext;
private final FhirProxyContext proxyContext;
private boolean logRequests;


public DSFFhirWebClientProvider(FhirContext fhirContext, String webserviceBaseUrl, int webserviceReadTimeout,
int webserviceConnectTimeout, String websocketUrl,
FhirSecurityContextProvider securityContextProvider,
FhirProxyContext proxyContext) {
FhirProxyContext proxyContext,
boolean logRequests) {
this.fhirContext = fhirContext;
this.webserviceBaseUrl = webserviceBaseUrl;
this.webserviceReadTimeout = webserviceReadTimeout;
this.webserviceConnectTimeout = webserviceConnectTimeout;
this.websocketUrl = websocketUrl;
this.securityContextProvider = securityContextProvider;
this.proxyContext = proxyContext;
this.logRequests = logRequests;
}

@Override
Expand All @@ -76,7 +79,7 @@ public FhirWebserviceClient provideFhirWebserviceClient() throws FhirWebClientPr
proxyContext.getPassword(),
webserviceConnectTimeout,
webserviceReadTimeout,
true,
logRequests,
null,
fhirContext,
cleaner);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public class DSFSpringConfig {
@Value("${app.broker.dsf.webservice.connectTimeout}")
private int webserviceConnectTimeout;

@Value("${app.broker.dsf.webservice.logRequests}")
private boolean logRequests;

@Value("${app.broker.dsf.websocket.url}")
private String websocketUrl;

Expand Down Expand Up @@ -102,7 +105,7 @@ FhirWebClientProvider fhirWebClientProvider(FhirContext fhirContext,
FhirSecurityContextProvider securityContextProvider,
FhirProxyContext proxyContext) {
return new DSFFhirWebClientProvider(fhirContext, webserviceBaseUrl, webserviceReadTimeout,
webserviceConnectTimeout, websocketUrl, securityContextProvider, proxyContext);
webserviceConnectTimeout, websocketUrl, securityContextProvider, proxyContext, logRequests);
}

}
5 changes: 5 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ app:
mock:
enabled: ${BROKER_CLIENT_MOCK_ENABLED:false}
direct:
auth:
basic:
username: ${BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME:}
password: ${BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD:}
enabled: ${BROKER_CLIENT_DIRECT_ENABLED:false}
useCql: ${BROKER_CLIENT_DIRECT_USE_CQL:false}
obfuscateResultCount: ${BROKER_CLIENT_OBFUSCATE_RESULT_COUNT:false}
Expand All @@ -79,6 +83,7 @@ app:
baseUrl: ${DSF_WEBSERVICE_BASE_URL}
readTimeout: 20000
connectTimeout: 2000
logRequests: ${DSF_WEBSERVICE_LOG_REQUESTS:false}
websocket:
url: ${DSF_WEBSOCKET_URL}
organizationId: ${DSF_ORGANIZATION_ID}
Expand Down
6 changes: 5 additions & 1 deletion src/main/resources/static/v3/api-docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -823,9 +823,13 @@ components:
format: int64
label:
type: string
created_at:
comment:
type: string
createdAt:
type: string
format: 'date-time'
totalNumberOfPatients:
type: integer
Query:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.numcodex.feasibility_gui_backend.query.broker.direct;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.function.client.WebClient;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import static org.assertj.core.api.Assertions.assertThat;


@ExtendWith(MockitoExtension.class)
public class DirectSpringConfigIT {

private static final String USERNAME = "some-user-123";
private static final String PASSWORD = "vALBAi95WW84x3";
MockWebServer mockWebServer;

private DirectSpringConfig directSpringConfig;

@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}

@AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}

@Test
void testDirectWebClientFlare_withCredentials() throws InterruptedException {
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("Foo"));
directSpringConfig = new DirectSpringConfig(true, String.format("http://localhost:%s", mockWebServer.getPort()), null, USERNAME, PASSWORD);
var authHeaderValue = "Basic " + Base64.getEncoder().encodeToString((USERNAME + ":" + PASSWORD).getBytes(StandardCharsets.UTF_8));

WebClient webClient = directSpringConfig.directWebClientFlare();

webClient
.get()
.uri("/foo")
.retrieve()
.bodyToMono(String.class)
.subscribe(responseBody -> {
})
;
var recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo(authHeaderValue);
}

@Test
void testDirectWebClientFlare_withoutCredentials() throws InterruptedException {
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("Foo"));
directSpringConfig = new DirectSpringConfig(true, String.format("http://localhost:%s", mockWebServer.getPort()), null, null, null);

WebClient webClient = directSpringConfig.directWebClientFlare();

webClient
.get()
.uri("/foo")
.retrieve()
.bodyToMono(String.class)
.subscribe(responseBody -> {
})
;
var recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull();
}

}
Loading

0 comments on commit 7487cbc

Please sign in to comment.