Skip to content

Commit 7f21cac

Browse files
authored
Merge pull request #1013 from qbicsoftware/development
Release PR
2 parents 0e3ab8d + 4124308 commit 7f21cac

File tree

153 files changed

+6885
-3187
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+6885
-3187
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Any local PKCS12 stores
2+
*.p12
3+
14
*/target/
25
.idea/
36
.settings

README.md

+93-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ that enables FAIR-compliant data access.
4545

4646
## Style Guide
4747

48-
Please find the [styleguide](https://github.com/qbicsoftware/data-manager-app-design-system) in its own repository.
48+
Please find the [styleguide](https://github.com/qbicsoftware/data-manager-app-design-system) in its
49+
own repository.
4950

5051
## How to Run
5152

@@ -124,7 +125,8 @@ spring.mail.host=${MAIL_HOST:smtp.gmail.com}
124125
spring.mail.port=${MAIL_PORT:587}
125126
```
126127

127-
For user email confirmation a specific endpoint and context-path (for example if the app runs in a different context than the root path) is addressed. This endpoint can be configured using
128+
For user email confirmation a specific endpoint and context-path (for example if the app runs in a
129+
different context than the root path) is addressed. This endpoint can be configured using
128130
the following properties:
129131

130132
| environment variable | description |
@@ -219,6 +221,93 @@ openbis.datasource.url=${OPENBIS_DATASOURCE_URL:openbis-url}
219221

220222
```
221223

224+
### Secret handling
225+
226+
Data Manager uses a custom vault concept for storing application secrets, that builds upon
227+
a [Java Keystore](https://docs.oracle.com/javase/17/docs/api/java/security/KeyStore.html) in its
228+
core.
229+
230+
We currently stick to a PKCS12 store type and encrypt every secret with AES.
231+
232+
Therefore, in order to be able to run the application, a keystore must be set-up beforehand.
233+
234+
#### Setup keystore
235+
236+
Before the Java keystore can be referenced in Data Manager's configuration, it has to be created in the first place.
237+
238+
You will need [keytool](https://docs.oracle.com/en/java/javase/11/tools/keytool.html) for this step.
239+
240+
Start the setup with a dummy entry for creation of a keystore file in PKSC12 format:
241+
242+
```bash
243+
keytool -genkeypair -alias dummy -keyalg RSA -keysize 2048 -keystore keystore.p12 -storetype PKCS12 -storepass mysecretpassword -dname "CN=Dummy, OU=Test, O=Company, L=City, ST=State, C=US"
244+
```
245+
246+
This secures the keystore with the `mysecretpassword` password. Change it to something only you have
247+
access and with
248+
Please choose a strong password. The application will fail for passwords with entropy below 100. The entropy of your password is calculated as follows
249+
250+
$$
251+
H = -\sum_{i=1}^{n} P(x_i) \log_2 P(x_i) \times n > 100.,
252+
$$
253+
254+
$$
255+
\begin{aligned}
256+
\text{where } & \text{ P(x_i) is the probability of character } x_i, \\
257+
& n \text{ is the total length of the password}.
258+
\end{aligned}
259+
$$
260+
$$
261+
H = -\sum_{i=1}^{n} P(x_i) \log_2 P(x_i) \times n > 100.,
262+
$$
263+
264+
$$
265+
\begin{aligned}
266+
\text{where } & \text{ P(x_i) is the probability of character } x_i, \\
267+
& n \text{ is the total length of the password}.
268+
\end{aligned}
269+
$$
270+
271+
The application will fail starting, if the total entropy is below 100.
272+
273+
Now remove the dummy entry, to have a true empty keystore:
274+
275+
```bash
276+
keytool -delete -alias dummy -keystore keystore.p12 -storetype PKCS12 -storepass mysecretpassword
277+
```
278+
279+
Verify:
280+
281+
```bash
282+
keytool -list -keystore keystore.p12 -storetype PKCS12 -storepass mysecretpassword
283+
```
284+
which should show something like:
285+
```text
286+
Keystore type: PKCS12
287+
Keystore provider: SUN
288+
289+
Your keystore contains 0 entries
290+
```
291+
292+
#### Configure vault in Data Manager
293+
294+
You need these three properties configured properly to operate the vault within the app. The secret
295+
for the vault is available in a local environment variable `DATAMANAGER_VAULT_KEY` and get more
296+
hardened with future releases.
297+
298+
For the secret entries themselves, define a strong secret in the `DATAMANAGER_VAULT_ENTRY_PASSWORD`
299+
environment variable. The same
300+
strength requirements apply here as well.
301+
302+
```properties
303+
# Sets the environment variable that contains the vault key
304+
qbic.security.vault.key.env=DATAMANAGER_VAULT_KEY
305+
# Since it will be a PKCS12 file, let the file end with *.p12
306+
qbic.security.vault.path=${DATAMANAGER_VAULT_PATH:keystore.p12}
307+
# The password used for encrypting entries in the vault. Must be longer than 20 characters.
308+
qbic.security.vault.entry.password.env=DATAMANAGER_VAULT_ENTRY_PASSWORD
309+
```
310+
222311
### Local testing
223312

224313
The default configuration of the app binds to the local port 8080 to the systems localhost. \
@@ -346,4 +435,5 @@ This work is licensed under the [MIT license](https://mit-license.org/).
346435
This work uses the [Vaadin Framework](https://github.com/vaadin), which is licensed
347436
under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).
348437

349-
The University of Tübingen logo is a registered trademark and the copyright is owned by the [University of Tübingen](https://uni-tuebingen.de/).
438+
The University of Tübingen logo is a registered trademark and the copyright is owned by
439+
the [University of Tübingen](https://uni-tuebingen.de/).

finances-infrastructure/pom.xml

-6
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@
2727
<groupId>org.springframework.security</groupId>
2828
<artifactId>spring-security-core</artifactId>
2929
</dependency>
30-
<dependency>
31-
<groupId>life.qbic.identity</groupId>
32-
<artifactId>project-management-infrastructure</artifactId>
33-
<version>0.34.0</version>
34-
<scope>compile</scope>
35-
</dependency>
3630
</dependencies>
3731

3832
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package life.qbic.projectmanagement.infrastructure;
2+
3+
import static life.qbic.logging.service.LoggerFactory.logger;
4+
5+
import java.io.BufferedOutputStream;
6+
import java.io.FileOutputStream;
7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
import java.security.KeyStore;
12+
import java.security.KeyStore.PasswordProtection;
13+
import java.security.KeyStore.SecretKeyEntry;
14+
import java.security.KeyStoreException;
15+
import java.security.NoSuchAlgorithmException;
16+
import java.security.UnrecoverableKeyException;
17+
import java.security.cert.CertificateException;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
import javax.crypto.spec.SecretKeySpec;
22+
import life.qbic.logging.api.Logger;
23+
import org.springframework.beans.factory.DisposableBean;
24+
import org.springframework.beans.factory.annotation.Value;
25+
import org.springframework.stereotype.Component;
26+
27+
/**
28+
* Container for a Java keystore to maintain secrets within the application.
29+
*
30+
* @since 1.8.0
31+
*/
32+
@Component
33+
public class DataManagerVault implements DisposableBean {
34+
35+
public static final String UNEXPECTED_VAULT_EXCEPTION = "Unexpected vault exception";
36+
private static final Logger log = logger(DataManagerVault.class);
37+
private static final String KEY_GENERATOR_ALGORITHM = "AES";
38+
private static final double MIN_ENTROPY = 100; // Shannon entropy * length of secret
39+
private final KeyStore keyStore;
40+
private final String envVarKeystorePassword;
41+
private final String envVarKeystoreEntryPassword;
42+
private final Path keystorePath;
43+
44+
public DataManagerVault(@Value("${qbic.security.vault.key.env}") String vaultKeyEnvVar,
45+
@Value("${qbic.security.vault.path}") String vaultPathString,
46+
@Value("${qbic.security.vault.entry.password.env}") String vaultEntryPassword)
47+
throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {
48+
if (System.getenv(vaultKeyEnvVar) == null) {
49+
throw new DataManagerVaultException(
50+
"Cannot find value for environment variable: %s".formatted(vaultKeyEnvVar));
51+
}
52+
if (System.getenv(vaultEntryPassword) == null) {
53+
throw new DataManagerVaultException(
54+
"Cannot find value for environment variable: %s".formatted(vaultEntryPassword)
55+
);
56+
}
57+
this.envVarKeystoreEntryPassword = vaultEntryPassword;
58+
this.envVarKeystorePassword = vaultKeyEnvVar;
59+
60+
double entropy;
61+
if ((entropy = calculateEntropy(System.getenv(envVarKeystorePassword))) < MIN_ENTROPY) {
62+
throw new DataManagerVaultException(
63+
"Entry of password for keystore was to low: %f (min: %f)".formatted(entropy,
64+
MIN_ENTROPY));
65+
}
66+
if ((entropy = calculateEntropy(System.getenv(envVarKeystoreEntryPassword))) < MIN_ENTROPY) {
67+
throw new DataManagerVaultException(
68+
"Entry of password for keystore entries was to low: %f (min: %f)".formatted(entropy,
69+
MIN_ENTROPY));
70+
}
71+
72+
this.keystorePath = fromString(vaultPathString);
73+
this.keyStore = createVault(vaultKeyEnvVar, keystorePath);
74+
}
75+
76+
// Calculates the product of Shannon entropy and secret length
77+
// See https://en.wikipedia.org/wiki/Entropy_(information_theory)
78+
private static double calculateEntropy(String secret) {
79+
if (secret == null || secret.isEmpty()) {
80+
return 0.0;
81+
}
82+
83+
Map<Character, Integer> frequencyMap = new HashMap<>();
84+
int length = secret.length();
85+
86+
// Count character frequencies
87+
for (char c : secret.toCharArray()) {
88+
frequencyMap.put(c, frequencyMap.getOrDefault(c, 0) + 1);
89+
}
90+
91+
// Compute entropy
92+
double entropy = 0.0;
93+
for (Integer count : frequencyMap.values()) {
94+
double probability = (double) count / length;
95+
entropy += probability * (Math.log(probability) / Math.log(2));
96+
}
97+
98+
return -entropy * secret.length(); // Negate since log probabilities are negative
99+
}
100+
101+
private static Path fromString(String path) {
102+
Path p = Paths.get(path);
103+
if (p.isAbsolute()) {
104+
return p;
105+
}
106+
return Path.of(System.getProperty("user.dir")).resolve(path);
107+
}
108+
109+
private static KeyStore createVault(String vaultKeyEnvVar, Path vaultPath)
110+
throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {
111+
112+
return KeyStore.getInstance(vaultPath.toFile(),
113+
System.getenv(vaultKeyEnvVar).toCharArray());
114+
}
115+
116+
/**
117+
* Adds a secret under a given alias to the vault and stores the vault content into the configured
118+
* file.
119+
* <p>
120+
* {@link DataManagerVault} applies an AES encryption on the provided secret.
121+
*
122+
* @param alias the reference the entry can be retrieved again with
123+
* {@link DataManagerVault#read(String)}.
124+
* @param secret the secret to store in the vault
125+
* @since 1.8.0
126+
*/
127+
public void add(String alias, String secret) {
128+
try {
129+
this.keyStore.setEntry(alias, new SecretKeyEntry(new SecretKeySpec(secret.getBytes(
130+
StandardCharsets.UTF_8), KEY_GENERATOR_ALGORITHM)),
131+
new PasswordProtection(System.getenv(envVarKeystoreEntryPassword).toCharArray()));
132+
} catch (KeyStoreException e) {
133+
throw new DataManagerVaultException(UNEXPECTED_VAULT_EXCEPTION, e);
134+
}
135+
136+
writeToFileSystem();
137+
}
138+
139+
private void writeToFileSystem() {
140+
try (var bos = new BufferedOutputStream(new FileOutputStream(keystorePath.toFile()))) {
141+
keyStore.store(bos, System.getenv(envVarKeystorePassword).toCharArray());
142+
bos.flush();
143+
} catch (IOException e) {
144+
throw new DataManagerVaultException("Unexpected vault exception when writing to the keystore",
145+
e);
146+
} catch (CertificateException | KeyStoreException | NoSuchAlgorithmException e) {
147+
throw new DataManagerVaultException(UNEXPECTED_VAULT_EXCEPTION, e);
148+
}
149+
}
150+
151+
/**
152+
* Read looks for a matching entry for the provided alias.
153+
* <p>
154+
* If no entry for the given alias is found, the vault returns an {@link Optional#empty()}.
155+
* <p>
156+
* If the decryption of the secret fails, an {@link DataManagerVaultException} is thrown.
157+
*
158+
* @param alias the reference for the entry to retrieve
159+
* @return an {@link Optional<String>} with the potential secret.
160+
* @throws DataManagerVaultException if the decryption fails.
161+
* @since 1.8.0
162+
*/
163+
public Optional<String> read(String alias) throws DataManagerVaultException {
164+
try {
165+
return Optional.ofNullable(
166+
this.keyStore.getKey(alias, System.getenv(envVarKeystoreEntryPassword).toCharArray()))
167+
.map(k -> new String(k.getEncoded(), StandardCharsets.UTF_8));
168+
} catch (KeyStoreException | NoSuchAlgorithmException e) {
169+
throw new DataManagerVaultException(UNEXPECTED_VAULT_EXCEPTION, e);
170+
} catch (UnrecoverableKeyException e) {
171+
throw new DataManagerVaultException("Recovering alias entry failed", e);
172+
}
173+
}
174+
175+
@Override
176+
public void destroy() throws Exception {
177+
log.debug("Destroying vault keystore " + this);
178+
// Ensure current cached entries are written to the file system
179+
writeToFileSystem();
180+
}
181+
182+
/**
183+
* Used for exceptions occurring during interactions with the {@link DataManagerVault}.
184+
*
185+
* @since 1.8.0
186+
*/
187+
public static class DataManagerVaultException extends RuntimeException {
188+
189+
public DataManagerVaultException(String message) {
190+
super(message);
191+
}
192+
193+
public DataManagerVaultException(String message, Throwable cause) {
194+
super(message, cause);
195+
}
196+
}
197+
198+
}

project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/batch/BatchJpaRepository.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import life.qbic.projectmanagement.domain.model.experiment.ExperimentId;
1515
import life.qbic.projectmanagement.domain.model.sample.Sample;
1616
import life.qbic.projectmanagement.domain.repository.BatchRepository;
17-
import life.qbic.projectmanagement.infrastructure.sample.QbicSampleRepository;
17+
import life.qbic.projectmanagement.infrastructure.sample.SampleJpaRepository;
1818
import org.springframework.beans.factory.annotation.Autowired;
1919
import org.springframework.stereotype.Repository;
2020

@@ -31,11 +31,11 @@ public class BatchJpaRepository implements BatchRepository {
3131
private static final Logger log = logger(BatchJpaRepository.class);
3232

3333
private final QbicBatchRepo qbicBatchRepo;
34-
private final QbicSampleRepository qbicSampleRepository;
34+
private final SampleJpaRepository qbicSampleRepository;
3535

3636
@Autowired
3737
public BatchJpaRepository(QbicBatchRepo qbicBatchRepo,
38-
QbicSampleRepository qbicSampleRepository) {
38+
SampleJpaRepository qbicSampleRepository) {
3939
this.qbicBatchRepo = qbicBatchRepo;
4040
this.qbicSampleRepository = qbicSampleRepository;
4141
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package life.qbic.projectmanagement.infrastructure.confounding;
2+
3+
import java.util.Collection;
4+
import java.util.List;
5+
import life.qbic.projectmanagement.domain.model.confounding.jpa.ConfoundingVariableData;
6+
import org.springframework.data.repository.ListCrudRepository;
7+
8+
public interface ConfoundingVariableJpaRepository extends
9+
ListCrudRepository<ConfoundingVariableData, Long> {
10+
11+
long countByExperimentIdEquals(String experimentId);
12+
13+
List<ConfoundingVariableData> findAllByExperimentIdEquals(String experimentId);
14+
15+
boolean existsDistinctByIdIsIn(Collection<Long> ids);
16+
}

0 commit comments

Comments
 (0)