Skip to content

Commit a1824f3

Browse files
authored
Merge pull request #846 from medizininformatik-initiative/release/v8.4.0
Release v8.4.0
2 parents 16dc0a3 + fdabceb commit a1824f3

12 files changed

Lines changed: 131 additions & 30 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ jobs:
164164
needs: tests
165165
runs-on: ubuntu-latest
166166
env:
167-
ONTOLOGY_GIT_TAG: v3.9.5
167+
ONTOLOGY_GIT_TAG: v3.9.6
168168
ELASTIC_HOST: http://localhost:9200
169169
ELASTIC_FILEPATH: https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/download/TAGPLACEHOLDER/
170170
ELASTIC_FILENAME: elastic.zip

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
44

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

7+
## [8.4.0] - 2026-02-10
8+
9+
- Based on ontology **[v3.9.6](https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/tag/v3.9.6)**
10+
11+
### Changed
12+
- Update ontology to [v3.9.6](https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/tag/v3.9.6) ([#844](https://github.com/medizininformatik-initiative/dataportal-backend/issues/844))
13+
### Fixed
14+
- Check children fields when validating CRTDL and checking attributeRefs ([#842](https://github.com/medizininformatik-initiative/dataportal-backend/issues/842))
15+
- DSF Client Key PW may be null ([#776](https://github.com/medizininformatik-initiative/dataportal-backend/issues/776))
16+
### Security
17+
- update dependencies
18+
719
## [8.3.0] - 2026-02-06
820

921
- Based on ontology **[v3.9.5](https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/tag/v3.9.5)**

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM eclipse-temurin:25.0.1_8-jre-alpine@sha256:9c65fe190cb22ba92f50b8d29a749d0f1cfb2437e09fe5fbf9ff927c45fc6e99
1+
FROM eclipse-temurin:25.0.2_10-jre-alpine@sha256:f10d6259d0798c1e12179b6bf3b63cea0d6843f7b09c9f9c9c422c50e44379ec
22

33
WORKDIR /opt/dataportal-backend
44

docker-compose.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,11 @@ services:
140140
environment:
141141
ES_HOST: http://dataportal-elastic
142142
ES_PORT: 9200
143-
ONTO_GIT_TAG: v3.9.5
143+
ONTO_GIT_TAG: v3.9.6
144144
ONTO_REPO: https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/download
145145
DOWNLOAD_FILENAME: elastic.zip
146146
EXIT_ON_EXISTING_INDICES: false
147147

148148
volumes:
149149
dataportal-postgres-data:
150-
name: "dataportal-postgres-data"
151-
dataportal-elastic-data:
152-
name: "dataportal-elastic-data"
150+
dataportal-elastic-data:

pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<groupId>de.medizininformatik-initiative</groupId>
1414
<artifactId>DataportalBackend</artifactId>
15-
<version>8.3.0</version>
15+
<version>8.4.0</version>
1616

1717
<name>Dataportal Backend</name>
1818
<description>Backend of the Dataportal</description>
@@ -27,7 +27,7 @@
2727
<java.version>17</java.version>
2828
<mockwebserver.version>5.3.2</mockwebserver.version>
2929
<okhttp3.version>4.10.0</okhttp3.version>
30-
<ontology-tag>v3.9.5</ontology-tag>
30+
<ontology-tag>v3.9.6</ontology-tag>
3131
</properties>
3232

3333
<dependencies>
@@ -103,7 +103,7 @@
103103
<dependency>
104104
<groupId>com.nimbusds</groupId>
105105
<artifactId>oauth2-oidc-sdk</artifactId>
106-
<version>11.31.1</version>
106+
<version>11.33</version>
107107
</dependency>
108108

109109
<dependency>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package de.medizininformatikinitiative.dataportal.backend.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.scheduling.annotation.EnableAsync;
5+
6+
@Configuration
7+
@EnableAsync
8+
public class AsyncConfig {
9+
}

src/main/java/de/medizininformatikinitiative/dataportal/backend/query/api/validation/DataExtractionValidator.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import de.medizininformatikinitiative.dataportal.backend.common.api.TermCode;
55
import de.medizininformatikinitiative.dataportal.backend.dse.DseService;
66
import de.medizininformatikinitiative.dataportal.backend.dse.api.DseProfile;
7+
import de.medizininformatikinitiative.dataportal.backend.dse.api.Field;
78
import de.medizininformatikinitiative.dataportal.backend.query.api.Attribute;
89
import de.medizininformatikinitiative.dataportal.backend.query.api.AttributeGroup;
910
import de.medizininformatikinitiative.dataportal.backend.query.api.DataExtraction;
@@ -62,6 +63,20 @@ public boolean isValid(DataExtraction dataExtraction,
6263
return !containsInvalidEntries(ctx, dataExtraction);
6364
}
6465

66+
private boolean fieldOrChildrenMatch(Field field, String attributeRef) {
67+
68+
if (field.id().equalsIgnoreCase(attributeRef)) {
69+
return true;
70+
}
71+
72+
if (field.children() != null && !field.children().isEmpty()) {
73+
return field.children().stream()
74+
.anyMatch(child -> fieldOrChildrenMatch(child, attributeRef));
75+
}
76+
77+
return false;
78+
}
79+
6580
private boolean containsInvalidEntries(ConstraintValidatorContext ctx, DataExtraction dataExtraction) {
6681
if (dataExtraction == null || dataExtraction.attributeGroups() == null || dataExtraction.attributeGroups().isEmpty()) return false;
6782
var hasErrors = false;
@@ -109,8 +124,9 @@ private boolean attributesContainErrors(ConstraintValidatorContext ctx,
109124
var attribute = attributeGroup.attributes().get(i);
110125

111126
if (dseProfile != null) {
112-
// ensure linkedGroup exists for each attribute of type "reference" in dse profile
127+
113128
if (dseProfile.references().stream().anyMatch(ref -> ref.id().equalsIgnoreCase(attribute.attributeRef()))) {
129+
// ensure linkedGroup exists for each attribute of type "reference" in dse profile
114130
if (attribute.linkedGroups() == null || attribute.linkedGroups().isEmpty()) {
115131
ValidationErrorBuilder.addError(
116132
ctx,
@@ -119,7 +135,7 @@ private boolean attributesContainErrors(ConstraintValidatorContext ctx,
119135
);
120136
hasErrors = true;
121137
}
122-
} else if (dseProfile.fields().stream().noneMatch(field -> field.id().equalsIgnoreCase(attribute.attributeRef()))) {
138+
} else if (dseProfile.fields().stream().noneMatch(field -> fieldOrChildrenMatch(field, attribute.attributeRef()))) {
123139
// attributes of DSE features (attributeGroups) (fields) not in ontology profile for feature
124140
ValidationErrorBuilder.addError(
125141
ctx,

src/main/java/de/medizininformatikinitiative/dataportal/backend/query/broker/dsf/DSFFhirSecurityContextProvider.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@
33
import de.rwh.utils.crypto.CertificateHelper;
44
import de.rwh.utils.crypto.io.CertificateReader;
55
import de.rwh.utils.crypto.io.PemIo;
6+
import org.apache.commons.text.RandomStringGenerator;
67

78
import java.io.IOException;
89
import java.nio.file.Files;
910
import java.nio.file.Paths;
1011
import java.security.cert.Certificate;
1112

13+
import static com.google.common.base.Strings.isNullOrEmpty;
14+
1215
/**
1316
* An entity that can provide a security context for communicating with a FHIR server.
1417
*/
1518
public class DSFFhirSecurityContextProvider implements FhirSecurityContextProvider {
1619

1720
private final String clientKeyFile;
18-
private final char[] clientKeyPassword;
21+
private final String clientKeyPassword;
1922
private final String trustCertificateFile;
2023
private String clientCertificateFile;
2124

22-
public DSFFhirSecurityContextProvider(String clientCertificateFile, String clientKeyFile, char[] clientKeyPassword,
25+
public DSFFhirSecurityContextProvider(String clientCertificateFile, String clientKeyFile, String keyStorePassword,
2326
String trustCertificateFile) {
2427
this.clientCertificateFile = clientCertificateFile;
2528
this.clientKeyFile = clientKeyFile;
26-
this.clientKeyPassword = clientKeyPassword;
29+
this.clientKeyPassword = keyStorePassword;
2730
this.trustCertificateFile = trustCertificateFile;
2831
}
2932

@@ -38,18 +41,20 @@ public FhirSecurityContext provideSecurityContext() throws FhirSecurityContextPr
3841
if (!Files.isReadable(localClientKeyFile)) {
3942
throw new IOException("Client key file '" + localClientKeyFile + "' not readable");
4043
}
44+
var password = RandomStringGenerator.builder().withinRange(33, 126).get().generate(16).toCharArray();
4145
var localKeyStore = CertificateHelper.toJksKeyStore(
42-
PemIo.readPrivateKeyFromPem(localClientKeyFile, clientKeyPassword),
46+
isNullOrEmpty(clientKeyPassword) ? PemIo.readPrivateKeyFromPem(localClientKeyFile)
47+
: PemIo.readPrivateKeyFromPem(localClientKeyFile, clientKeyPassword.toCharArray()),
4348
new Certificate[]{PemIo.readX509CertificateFromPem(localClientCertificateFile)},
44-
"backend-cert", clientKeyPassword);
49+
"backend-cert", password);
4550
var localTrustCertificateFile = Paths.get(trustCertificateFile);
4651
if (!Files.isReadable(localTrustCertificateFile)) {
4752
throw new IOException("Certificate file '" + trustCertificateFile + "' not readable");
4853
}
4954
var localTrustStore = CertificateReader.allFromCer(localTrustCertificateFile);
5055

5156

52-
return new FhirSecurityContext(localKeyStore, localTrustStore, clientKeyPassword);
57+
return new FhirSecurityContext(localKeyStore, localTrustStore, password);
5358
} catch (Exception e) {
5459
throw new FhirSecurityContextProvisionException(e);
5560
}

src/main/java/de/medizininformatikinitiative/dataportal/backend/query/broker/dsf/DSFSpringConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public class DSFSpringConfig {
2323
@Value("${app.broker.dsf.security.client.key.file}")
2424
private String clientKeyFile;
2525

26-
@Value("${app.broker.dsf.security.client.key.password}")
27-
private char[] keyStorePassword;
26+
@Value("${app.broker.dsf.security.client.key.password:#{null}}")
27+
private String keyStorePassword;
2828

2929
@Value("${app.broker.dsf.security.certificate}")
3030
private String certificateFile;

src/test/java/de/medizininformatikinitiative/dataportal/backend/query/broker/dsf/DSFFhirSecurityContextProviderTest.java

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
public class DSFFhirSecurityContextProviderTest {
1515

16-
private static final char[] PASSWORD = "password".toCharArray();
16+
private static final String PASSWORD = "password";
1717
static private CertificateFactory cf;
1818

1919
@BeforeAll
@@ -23,10 +23,10 @@ static void setup() throws Exception {
2323

2424
@Test
2525
void trustStoreContainsCertificateFromPEM() throws Exception {
26-
DSFFhirSecurityContextProvider securityContextProvider = new DSFFhirSecurityContextProvider(
26+
var securityContextProvider = new DSFFhirSecurityContextProvider(
2727
getFilePath("client.crt"), getFilePath("client.key"), PASSWORD, getFilePath("foo.pem"));
2828

29-
FhirSecurityContext securityContext = securityContextProvider.provideSecurityContext();
29+
var securityContext = securityContextProvider.provideSecurityContext();
3030

3131
assertThat(getCertificateAlias(securityContext, "foo.pem")).isNotBlank();
3232
}
@@ -44,10 +44,10 @@ void trustStoreContainsMultipleCertificatesFromPEM() throws Exception {
4444

4545
@Test
4646
void keyStoreContainsClientCertificateAndKey() throws Exception {
47-
DSFFhirSecurityContextProvider securityContextProvider = new DSFFhirSecurityContextProvider(
47+
var securityContextProvider = new DSFFhirSecurityContextProvider(
4848
getFilePath("client.crt"), getFilePath("client.key"), PASSWORD, getFilePath("multiple.pem"));
4949

50-
FhirSecurityContext securityContext = securityContextProvider.provideSecurityContext();
50+
var securityContext = securityContextProvider.provideSecurityContext();
5151

5252
var alias = assertThat(Collections.list(securityContext.getKeyStore().aliases()))
5353
.hasSize(1)
@@ -58,7 +58,7 @@ void keyStoreContainsClientCertificateAndKey() throws Exception {
5858

5959
@Test
6060
void failsOnMissingClientCertificate() throws Exception {
61-
DSFFhirSecurityContextProvider securityContextProvider = new DSFFhirSecurityContextProvider(
61+
var securityContextProvider = new DSFFhirSecurityContextProvider(
6262
"nonexisting.crt", getFilePath("client.key"), PASSWORD, getFilePath("foo.pem"));
6363

6464
assertThatThrownBy(() -> securityContextProvider.provideSecurityContext()).cause()
@@ -67,7 +67,7 @@ void failsOnMissingClientCertificate() throws Exception {
6767

6868
@Test
6969
void failsOnMissingClientKey() throws Exception {
70-
DSFFhirSecurityContextProvider securityContextProvider = new DSFFhirSecurityContextProvider(
70+
var securityContextProvider = new DSFFhirSecurityContextProvider(
7171
getFilePath("client.crt"), "nonexisting.key", PASSWORD, getFilePath("foo.pem"));
7272

7373
assertThatThrownBy(() -> securityContextProvider.provideSecurityContext()).cause()
@@ -76,7 +76,7 @@ void failsOnMissingClientKey() throws Exception {
7676

7777
@Test
7878
void failsOnMissingCaCertificate() throws Exception {
79-
DSFFhirSecurityContextProvider securityContextProvider = new DSFFhirSecurityContextProvider(
79+
var securityContextProvider = new DSFFhirSecurityContextProvider(
8080
getFilePath("client.crt"), getFilePath("client.key"), PASSWORD, "nonexisting.pem");
8181

8282
assertThatThrownBy(() -> securityContextProvider.provideSecurityContext()).cause()
@@ -85,14 +85,47 @@ void failsOnMissingCaCertificate() throws Exception {
8585

8686
@Test
8787
void failsOnWrongClientKeyPassword() throws Exception {
88-
DSFFhirSecurityContextProvider securityContextProvider = new DSFFhirSecurityContextProvider(
89-
getFilePath("client.crt"), getFilePath("client.key"), "WrongPassword".toCharArray(),
88+
var securityContextProvider = new DSFFhirSecurityContextProvider(
89+
getFilePath("client.crt"), getFilePath("client.key"), "WrongPassword",
9090
getFilePath("foo.pem"));
9191

9292
assertThatThrownBy(() -> securityContextProvider.provideSecurityContext()).cause()
9393
.hasMessageContaining("unable to read encrypted data");
9494
}
9595

96+
@Test
97+
void unencryptedClientKeyWithEmptyPassword() throws Exception {
98+
var securityContextProvider = new DSFFhirSecurityContextProvider(
99+
getFilePath("client.crt"), getFilePath("unencryptedClient.key"), "",
100+
getFilePath("foo.pem"));
101+
102+
var securityContext = securityContextProvider.provideSecurityContext();
103+
104+
assertThat(getCertificateAlias(securityContext, "foo.pem")).isNotBlank();
105+
}
106+
107+
@Test
108+
void unencryptedClientKeyWithNullPassword() throws Exception {
109+
var securityContextProvider = new DSFFhirSecurityContextProvider(
110+
getFilePath("client.crt"), getFilePath("unencryptedClient.key"), null,
111+
getFilePath("foo.pem"));
112+
113+
var securityContext = securityContextProvider.provideSecurityContext();
114+
115+
assertThat(getCertificateAlias(securityContext, "foo.pem")).isNotBlank();
116+
}
117+
118+
@Test
119+
void unencryptedClientKeyWithPassword() throws Exception {
120+
var securityContextProvider = new DSFFhirSecurityContextProvider(
121+
getFilePath("client.crt"), getFilePath("unencryptedClient.key"), "foobar",
122+
getFilePath("foo.pem"));
123+
124+
var securityContext = securityContextProvider.provideSecurityContext();
125+
126+
assertThat(getCertificateAlias(securityContext, "foo.pem")).isNotBlank();
127+
}
128+
96129
private String getCertificateAlias(FhirSecurityContext securityContext, String fileName) throws Exception {
97130
InputStream resource = DSFFhirSecurityContextProviderTest.class.getResourceAsStream(fileName);
98131
Certificate cert = cf.generateCertificate(resource);

0 commit comments

Comments
 (0)