diff --git a/CITATION.cff b/CITATION.cff
index e2ea1e7f0..0c24355e3 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -24,8 +24,8 @@ preferred-citation:
doi: 10.3233/SHTI210060
type: proceedings
title: "Data Sharing Framework (DSF)"
-version: 2.1.1
-date-released: 2026-04-20
+version: 2.2.0
+date-released: 2026-08-25
url: https://dsf.dev
repository-code: https://github.com/datasharingframework/dsf
repository-artifact: https://github.com/datasharingframework/dsf/releases
diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java
index ec6cd03f2..687e37332 100644
--- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java
+++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java
@@ -47,11 +47,15 @@
import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory;
import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairValidator;
import de.hsheilbronn.mi.utils.crypto.keystore.KeyStoreCreator;
+import dev.dsf.bpe.v2.service.stream.LimitedInputStream;
public class CryptoServiceImpl implements CryptoService
{
public static final class KemDelegate implements Kem
{
+ protected static final long ENCRYPT_LIMIT = 250 * 1024 * 1024; // 250 MiB
+ protected static final long DECRYPT_LIMIT = (250 * 1024 * 1024) + 1024; // 250 MiB + 1024 Bytes
+
private final AbstractKemAesGcm delegate;
public KemDelegate(AbstractKemAesGcm delegate)
@@ -63,7 +67,7 @@ public KemDelegate(AbstractKemAesGcm delegate)
public InputStream encrypt(InputStream data, PublicKey publicKey) throws NoSuchAlgorithmException,
InvalidKeyException, NoSuchPaddingException, InvalidAlgorithmParameterException
{
- return delegate.encrypt(data, publicKey);
+ return delegate.encrypt(new LimitedInputStream(data, ENCRYPT_LIMIT), publicKey);
}
@Override
@@ -71,7 +75,7 @@ public InputStream decrypt(InputStream encrypted, PrivateKey privateKey)
throws IOException, NoSuchAlgorithmException, InvalidKeyException, DecapsulateException,
NoSuchPaddingException, InvalidAlgorithmParameterException
{
- return delegate.decrypt(encrypted, privateKey);
+ return delegate.decrypt(new LimitedInputStream(encrypted, DECRYPT_LIMIT), privateKey);
}
}
diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java
new file mode 100644
index 000000000..3b462af3f
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v2.service.stream;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+/**
+ * An InputStream wrapper that exposes at most a configured number of bytes from the underlying stream.
+ *
+ *
+ * Unlike a regular end-of-stream condition, exhausting the configured limit is treated as a resource limit violation.
+ * Once the configured limit has been exhausted, every subsequent call to {@link #read()} or {@link #skip(long)} throws
+ * {@link IOException}, regardless of whether the underlying stream has reached end-of-stream.
+ *
+ *
+ *
+ * If the underlying stream supports mark/reset, this stream supports it as well. The remaining byte limit is restored
+ * when {@link #reset()} is called.
+ *
+ *
+ * This class is not safe for concurrent use by multiple threads.
+ */
+public final class LimitedInputStream extends FilterInputStream
+{
+ private final long maxBytes;
+
+ private long remainingBytes;
+ private long markedRemaining;
+
+ /**
+ * Creates a stream that allows reading at most {@code maxBytes} bytes.
+ *
+ * @param in
+ * the underlying input stream
+ * @param maxBytes
+ * the maximum number of bytes that may be read
+ * @throws NullPointerException
+ * if {@code in} is null
+ * @throws IllegalArgumentException
+ * if {@code maxBytes} is negative
+ */
+ public LimitedInputStream(InputStream in, long maxBytes)
+ {
+ super(Objects.requireNonNull(in, "in"));
+
+ if (maxBytes < 0)
+ throw new IllegalArgumentException("maxBytes must be >= 0");
+
+ this.maxBytes = maxBytes;
+
+ this.remainingBytes = maxBytes;
+ this.markedRemaining = maxBytes;
+ }
+
+ /**
+ * Returns the number of bytes that may still be consumed.
+ *
+ * @return remaining byte allowance
+ */
+ public long getRemainingBytes()
+ {
+ return remainingBytes;
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ ensureRemaining();
+
+ int b = super.read();
+ if (b != -1)
+ remainingBytes--;
+
+ return b;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException
+ {
+ Objects.checkFromIndexSize(off, len, b.length);
+
+ if (len == 0)
+ return 0;
+
+ ensureRemaining();
+
+ int allowed = (int) Math.min(len, remainingBytes);
+ int count = super.read(b, off, allowed);
+
+ if (count > 0)
+ remainingBytes -= count;
+
+ return count;
+ }
+
+ /**
+ * Skipping bytes beyond the configured limit is treated as an attempt to consume data beyond the allowed resource
+ * boundary.
+ *
+ * @throws IOException
+ * if the limit has already been exhausted
+ */
+ @Override
+ public long skip(long n) throws IOException
+ {
+ if (n <= 0)
+ return 0;
+
+ ensureRemaining();
+
+ long skipped = super.skip(Math.min(n, remainingBytes));
+ remainingBytes -= skipped;
+
+ return skipped;
+ }
+
+ /**
+ * Returns the smaller of the underlying available bytes and the remaining byte allowance. This value represents
+ * bytes available within this wrapper's limit, not necessarily bytes immediately readable.
+ */
+ @Override
+ public int available() throws IOException
+ {
+ return (int) Math.min(super.available(), remainingBytes);
+ }
+
+ @Override
+ public synchronized void mark(int readlimit)
+ {
+ if (markSupported())
+ {
+ super.mark(readlimit);
+ markedRemaining = remainingBytes;
+ }
+ }
+
+ @Override
+ public synchronized void reset() throws IOException
+ {
+ super.reset();
+ remainingBytes = markedRemaining;
+ }
+
+ private void ensureRemaining() throws IOException
+ {
+ if (remainingBytes == 0)
+ throw new IOException("Stream limit of " + maxBytes + " byte" + (maxBytes != 1 ? "s" : "") + " exceeded");
+ }
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java
new file mode 100644
index 000000000..abd310225
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v2.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.KeyPair;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import de.hsheilbronn.mi.utils.crypto.kem.AbstractKemAesGcm;
+import de.hsheilbronn.mi.utils.crypto.kem.EcDhKemAesGcm;
+import de.hsheilbronn.mi.utils.crypto.kem.RsaKemAesGcm;
+import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory;
+import dev.dsf.bpe.v2.service.CryptoService.Kem;
+import dev.dsf.bpe.v2.service.CryptoServiceImpl.KemDelegate;
+
+@RunWith(Parameterized.class)
+public class CryptoServiceTest
+{
+ private static final class ZeroInputStream extends InputStream
+ {
+ private final long size;
+ private long position = 0;
+
+ public ZeroInputStream(long size)
+ {
+ this.size = size;
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ if (position >= size)
+ return -1;
+
+ position++;
+ return 0;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException
+ {
+ if (position >= size)
+ return -1;
+
+ int bytesToRead = (int) Math.min(len, size - position);
+
+ for (int i = 0; i < bytesToRead; i++)
+ b[off + i] = 0;
+
+ position += bytesToRead;
+ return bytesToRead;
+ }
+
+ @Override
+ public long skip(long n) throws IOException
+ {
+ long skipped = Math.min(n, size - position);
+ position += skipped;
+ return skipped;
+ }
+
+ @Override
+ public int available() throws IOException
+ {
+ long remaining = size - position;
+ return remaining > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) remaining;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ // nothing to do
+ }
+ }
+
+ @Parameters
+ public static Object[][] data()
+ {
+ CryptoService cryptoService = new CryptoServiceImpl();
+
+ return new Object[][] {
+ { cryptoService.createKeyPairGeneratorX25519AndInitialize().genKeyPair(), cryptoService.createEcDhKem(),
+ new EcDhKemAesGcm() },
+ { cryptoService.createKeyPairGeneratorX448AndInitialize().genKeyPair(), cryptoService.createEcDhKem(),
+ new EcDhKemAesGcm() },
+ { KeyPairGeneratorFactory.rsa1024().initialize().generateKeyPair(), cryptoService.createRsaKem(),
+ new RsaKemAesGcm() },
+ { cryptoService.createKeyPairGeneratorRsa4096AndInitialize().generateKeyPair(),
+ cryptoService.createRsaKem(), new RsaKemAesGcm() } };
+ }
+
+ @Parameter(0)
+ public KeyPair keyPair;
+
+ @Parameter(1)
+ public Kem kem;
+
+ @Parameter(2)
+ public AbstractKemAesGcm noLimitKem;
+
+ @Test
+ public void testEncryptionLimit() throws Exception
+ {
+ try
+ {
+ kem.encrypt(new ZeroInputStream(KemDelegate.ENCRYPT_LIMIT + 1), keyPair.getPublic())
+ .transferTo(OutputStream.nullOutputStream());
+
+ fail("Expected IOException");
+ }
+ catch (IOException e)
+ {
+ assertEquals("Stream limit of " + KemDelegate.ENCRYPT_LIMIT + " bytes exceeded", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDecryptLimit() throws Exception
+ {
+ try
+ {
+ InputStream encrypted = noLimitKem.encrypt(new ZeroInputStream(KemDelegate.DECRYPT_LIMIT + 1),
+ keyPair.getPublic());
+
+ kem.decrypt(encrypted, keyPair.getPrivate()).transferTo(OutputStream.nullOutputStream());
+
+ fail("Expected IOException");
+ }
+ catch (IOException e)
+ {
+ assertEquals("Stream limit of " + KemDelegate.DECRYPT_LIMIT + " bytes exceeded", e.getMessage());
+ }
+ }
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/stream/LimitedInputStreamTest.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/stream/LimitedInputStreamTest.java
new file mode 100644
index 000000000..20aaaf357
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/stream/LimitedInputStreamTest.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v2.service.stream;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.junit.Test;
+
+public class LimitedInputStreamTest
+{
+ @SuppressWarnings("resource")
+ @Test(expected = NullPointerException.class)
+ public void constructorRejectsNullInputStream()
+ {
+ new LimitedInputStream(null, 1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructorRejectsNegativeLimit()
+ {
+ new LimitedInputStream(new ByteArrayInputStream(new byte[0]), -1);
+ }
+
+ @Test
+ public void initialRemainingBytesEqualsConfiguredLimit()
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3 }), 2);
+
+ assertEquals(2, in.getRemainingBytes());
+ }
+
+ @Test
+ public void readSingleBytesConsumesLimit() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 10, 20, 30 }), 2);
+
+ assertEquals(10, in.read());
+ assertEquals(1, in.getRemainingBytes());
+
+ assertEquals(20, in.read());
+ assertEquals(0, in.getRemainingBytes());
+
+ try
+ {
+ in.read();
+ fail("Expected IOException");
+ }
+ catch (IOException e)
+ {
+ assertEquals("Stream limit of 2 bytes exceeded", e.getMessage());
+ }
+ }
+
+ @Test
+ public void readReturnsEndOfStreamWithoutConsumingRemainingLimit() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1 }), 5);
+
+ assertEquals(1, in.read());
+ assertEquals(4, in.getRemainingBytes());
+
+ assertEquals(-1, in.read());
+ assertEquals(4, in.getRemainingBytes());
+
+ assertEquals(-1, in.read());
+ assertEquals(4, in.getRemainingBytes());
+ }
+
+ @Test
+ public void bulkReadRespectsConfiguredLimit() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 }), 3);
+
+ byte[] buffer = new byte[10];
+
+ int count = in.read(buffer, 2, 5);
+
+ assertEquals(3, count);
+ assertArrayEquals(new byte[] { 0, 0, 1, 2, 3, 0, 0, 0, 0, 0 }, buffer);
+
+ assertEquals(0, in.getRemainingBytes());
+
+ try
+ {
+ in.read(buffer, 0, 1);
+ fail("Expected IOException");
+ }
+ catch (IOException e)
+ {
+ assertEquals("Stream limit of 3 bytes exceeded", e.getMessage());
+ }
+ }
+
+ @Test
+ public void zeroLengthReadReturnsZeroWithoutCheckingLimit() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1 }), 0);
+
+ byte[] buffer = new byte[4];
+
+ assertEquals(0, in.read(buffer, 0, 0));
+ assertEquals(0, in.getRemainingBytes());
+ }
+
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void bulkReadValidatesRange() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[1]), 1);
+
+ in.read(new byte[2], 1, 2);
+ }
+
+ @Test
+ public void skipConsumesRemainingLimit() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }), 3);
+
+ assertEquals(2, in.skip(2));
+ assertEquals(1, in.getRemainingBytes());
+
+ assertEquals(1, in.skip(5));
+ assertEquals(0, in.getRemainingBytes());
+
+ try
+ {
+ in.skip(1);
+ fail("Expected IOException");
+ }
+ catch (IOException e)
+ {
+ assertEquals("Stream limit of 3 bytes exceeded", e.getMessage());
+ }
+ }
+
+ @Test
+ public void skipWithNonPositiveValueReturnsZero() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3 }), 3);
+
+ assertEquals(0, in.skip(0));
+ assertEquals(0, in.skip(-5));
+ assertEquals(3, in.getRemainingBytes());
+ }
+
+ @Test
+ public void availableIsLimitedByRemainingBytes() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 }), 3);
+
+ assertEquals(3, in.available());
+
+ in.read();
+
+ assertEquals(2, in.available());
+ }
+
+ @Test
+ public void markAndResetRestoreRemainingBytes() throws Exception
+ {
+ ByteArrayInputStream source = new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 });
+
+ LimitedInputStream in = new LimitedInputStream(source, 4);
+
+ assertTrue(in.markSupported());
+
+ in.read();
+ assertEquals(3, in.getRemainingBytes());
+
+ in.mark(10);
+
+ in.read();
+ in.read();
+
+ assertEquals(1, in.getRemainingBytes());
+
+ in.reset();
+
+ assertEquals(3, in.getRemainingBytes());
+ assertEquals(2, in.read());
+ assertEquals(2, in.getRemainingBytes());
+ }
+
+ @Test
+ public void markDoesNothingWhenUnderlyingStreamDoesNotSupportMark() throws Exception
+ {
+ InputStream source = new InputStream()
+ {
+ private final byte[] data = { 1, 2, 3 };
+ private int index;
+
+ @Override
+ public int read()
+ {
+ return index < data.length ? data[index++] : -1;
+ }
+
+ @Override
+ public boolean markSupported()
+ {
+ return false;
+ }
+ };
+
+ @SuppressWarnings("resource")
+ LimitedInputStream in = new LimitedInputStream(source, 3);
+
+ assertFalse(in.markSupported());
+
+ in.read();
+ assertEquals(2, in.getRemainingBytes());
+
+ in.mark(100);
+
+ try
+ {
+ in.reset();
+ fail("Expected IOException");
+ }
+ catch (IOException expected)
+ {
+ // Expected from FilterInputStream/InputStream because mark is unsupported.
+ }
+ }
+
+ @Test
+ public void exceptionMessageUsesSingularForOneByteLimit() throws Exception
+ {
+ LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1 }), 1);
+
+ assertEquals(1, in.read());
+
+ try
+ {
+ in.read();
+ fail("Expected IOException");
+ }
+ catch (IOException e)
+ {
+ assertEquals("Stream limit of 1 byte exceeded", e.getMessage());
+ }
+ }
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/CryptoService.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/CryptoService.java
index bfe0c7dc6..23d100b47 100644
--- a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/CryptoService.java
+++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/CryptoService.java
@@ -63,7 +63,9 @@
public interface CryptoService
{
/**
- * Key encapsulation mechanism with encrypt and decrypt methods.
+ * Key encapsulation mechanism with encrypt and decrypt methods.
+ *
+ * Starting with DSF 2.1.1 plain-texts are limited to 250 MiB
*/
public interface Kem
{
diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java
index d7e108cb4..6cad0c156 100644
--- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java
+++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java
@@ -17,6 +17,7 @@
import java.sql.Connection;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -117,6 +118,12 @@ public void checkReadAllowed(int index, Connection connection, Identity identity
public void checkUpdateAllowed(int index, Connection connection, Identity identity, Resource oldResource,
Resource newResource) throws WebApplicationException
{
+ Objects.requireNonNull(newResource, "newResource");
+ Objects.requireNonNull(oldResource, "oldResource");
+ // intentionally guarding against object identity
+ if (newResource == oldResource)
+ throw new IllegalStateException("new resource same object as old resource");
+
final String resourceTypeName = getResourceTypeName(oldResource);
final String resourceId = oldResource.getIdElement().getIdPart();
final long resourceVersion = oldResource.getIdElement().getVersionIdPartAsLong();
diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java
index 42a056c23..7aa25a449 100755
--- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java
+++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java
@@ -30,6 +30,7 @@
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.PrimitiveType;
+import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.model.Type;
@@ -211,12 +212,13 @@ private void afterDelete(String id)
@Override
public Response postSnapshotNew(String snapshotPath, Parameters parameters, UriInfo uri, HttpHeaders headers)
{
- ParametersParameterComponent param = parameters.getParameter("url");
- Type urlType = param.getValue();
- Optional resource = parameters.getParameter().stream()
- .filter(p -> "resource".equals(p.getName())).findFirst();
+ ParametersParameterComponent urlParam = parameters.getParameter("url");
+ Type urlType = urlParam == null ? null : urlParam.getValue();
- if (urlType != null && resource.isEmpty())
+ ParametersParameterComponent resourceParam = parameters.getParameter("resource");
+ Resource resource = resourceParam == null ? null : resourceParam.getResource();
+
+ if (urlType != null && resource == null)
{
if (!(urlType instanceof StringType || urlType instanceof UriType))
return Response.status(Status.BAD_REQUEST).build(); // TODO OperationOutcome
@@ -228,25 +230,24 @@ public Response postSnapshotNew(String snapshotPath, Parameters parameters, UriI
return getSnapshot(url.getValue(), uri, headers);
}
- else if (urlType == null && resource.isPresent() && resource.get().getResource() != null)
+ else if (urlType == null && resource != null)
{
- if (!(resource.get().getResource() instanceof StructureDefinition))
+ if (!(resource instanceof StructureDefinition))
return Response.status(Status.BAD_REQUEST).build(); // TODO OperationOutcome
- StructureDefinition sd = (StructureDefinition) resource.get().getResource();
-
- logger.trace("Parameters with StructureDefinition resource url {}", sd.getUrl());
+ StructureDefinition sd = (StructureDefinition) resource;
- if (!sd.hasDifferential())
- return Response.status(Status.BAD_REQUEST).build(); // TODO OperationOutcome
+ logger.trace("Parameters with StructureDefinition.url {}", sd.getUrl());
if (sd.hasSnapshot())
return responseGenerator
.response(Status.OK, sd, parameterConverter.getMediaTypeThrowIfNotSupported(uri, headers))
.build();
- else
+ else if (sd.hasDifferential())
return responseGenerator.response(Status.OK, generateSnapshot(sd),
parameterConverter.getMediaTypeThrowIfNotSupported(uri, headers)).build();
+ else
+ return Response.status(Status.BAD_REQUEST).build(); // TODO OperationOutcome
}
else
{
@@ -257,8 +258,7 @@ else if (urlType == null && resource.isPresent() && resource.get().getResource()
private Response getSnapshot(String url, UriInfo uri, HttpHeaders headers)
{
- SearchQuery query = snapshotDao.createSearchQuery(getCurrentIdentity(),
- PageAndCount.single());
+ SearchQuery query = snapshotDao.createSearchQueryWithoutUserFilter(PageAndCount.single());
Map> searchParameters = new HashMap<>();
searchParameters.put(StructureDefinitionUrl.PARAMETER_NAME, List.of(url));
searchParameters.put(SearchQuery.PARAMETER_SORT, List.of("-" + ResourceLastUpdated.PARAMETER_NAME));
@@ -295,7 +295,7 @@ public Response getSnapshotExisting(String snapshotPath, String id, UriInfo uri,
() -> snapshotDao.read(parameterConverter.toUuid(resourceTypeName, id)), Optional::empty,
Optional::empty);
- if (snapshot.isPresent())
+ if (snapshot.isPresent() && snapshot.get().hasSnapshot())
return snapshot.map(d -> responseGenerator.response(Status.OK, d,
parameterConverter.getMediaTypeThrowIfNotSupported(uri, headers))).get().build();
diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java
index c9e4a491b..e3742372b 100755
--- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java
+++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java
@@ -94,7 +94,6 @@ public AbstractResourceServiceSecure(S delegate, String serverBase, ResponseGene
this.referenceCleaner = referenceCleaner;
this.referenceExtractor = referenceExtractor;
this.resourceType = resourceType;
- this.defaultProfileProvider = defaultProfileProvider;
this.resourceTypeName = resourceType.getAnnotation(ResourceDef.class).name();
this.dao = dao;
this.exceptionHandler = exceptionHandler;
@@ -102,6 +101,7 @@ public AbstractResourceServiceSecure(S delegate, String serverBase, ResponseGene
this.authorizationRule = authorizationRule;
this.resourceValidator = resourceValidator;
this.validationRules = validationRules;
+ this.defaultProfileProvider = defaultProfileProvider;
}
@Override
@@ -400,6 +400,12 @@ public Response update(String id, R resource, UriInfo uri, HttpHeaders headers)
private Response update(String id, R newResource, UriInfo uri, HttpHeaders headers, R oldResource)
{
+ Objects.requireNonNull(newResource, "newResource");
+ Objects.requireNonNull(oldResource, "oldResource");
+ // intentionally guarding against object identity
+ if (newResource == oldResource)
+ throw new IllegalStateException("new resource same object as old resource");
+
resolveLiteralInternalRelatedArtifactOrAttachmentUrls(newResource);
final String resourceId = oldResource.getIdElement().getIdPart();
@@ -454,20 +460,24 @@ public Response update(R resource, UriInfo uri, HttpHeaders headers)
Map> queryParameters = uri.getQueryParameters();
PartialResult result = getExisting(queryParameters);
- // No matches, no id provided: The server creates the resource.
- if (result.getTotal() <= 0 && !resource.hasId())
+ // No matches
+ if (result.getTotal() <= 0)
{
- // more security checks and audit log in create method
- return create(resource, uri, headers);
- }
+ // no id provided: The server creates the resource.
+ if (!resource.hasId())
+ {
+ // more security checks and audit log in create method
+ return create(resource, uri, headers);
+ }
- // No matches, id provided: The server treats the interaction as an Update as Create interaction (or rejects it,
- // if it does not support Update as Create) -> reject
- else if (result.getTotal() <= 0 && resource.hasId())
- {
- audit.info("Create as update of non existing {} denied for identity '{}'", resourceTypeName,
- getCurrentIdentity().getName());
- return responseGenerator.updateAsCreateNotAllowed(resourceTypeName);
+ // id provided: The server treats the interaction as an Update as Create interaction (or rejects it, if it
+ // does not support Update as Create) -> reject
+ else
+ {
+ audit.info("Create as update of non existing {} denied for identity '{}'", resourceTypeName,
+ getCurrentIdentity().getName());
+ return responseGenerator.updateAsCreateNotAllowed(resourceTypeName);
+ }
}
// One Match, no resource id provided OR (resource id provided and it matches the found resource):
@@ -482,7 +492,7 @@ else if (result.getTotal() == 1)
{
resource.setIdElement(dbResourceId);
// more security checks and audit log in update method
- return update(resource.getIdElement().getIdPart(), resource, uri, headers, resource);
+ return update(resource.getIdElement().getIdPart(), resource, uri, headers, dbResource);
}
// update: resource has same id
@@ -494,7 +504,7 @@ else if (resource.hasId()
&& dbResourceId.getIdPart().equals(resource.getIdElement().getIdPart()))
{
// more security checks and audit log in update method
- return update(resource.getIdElement().getIdPart(), resource, uri, headers, resource);
+ return update(resource.getIdElement().getIdPart(), resource, uri, headers, dbResource);
}
// update resource has different id -> 400 Bad Request
@@ -513,7 +523,7 @@ else if (resource.hasId()
// Multiple matches: The server returns a 412 Precondition Failed error indicating the client's criteria were
// not selective enough preferably with an OperationOutcome
- else // if (result.getOverallCount() > 1)
+ else
{
audit.info(
"Update of {} denied for identity '{}', conditional update criteria not selective enough, multiple matches",
@@ -617,7 +627,7 @@ public Response delete(UriInfo uri, HttpHeaders headers)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
- SearchQuery query = dao.createSearchQuery(getCurrentIdentity(), PageAndCount.single());
+ SearchQuery query = dao.createSearchQueryWithoutUserFilter(PageAndCount.single());
query.configureParameters(queryParameters);
List unsupportedQueryParameters = query.getUnsupportedQueryParameters();
diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecure.java
index 7a70db6b7..2e7fd4990 100755
--- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecure.java
+++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecure.java
@@ -16,7 +16,10 @@
package dev.dsf.fhir.webservice.secure;
import org.hl7.fhir.r4.model.Parameters;
+import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
+import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StructureDefinition;
+import org.hl7.fhir.r4.model.Type;
import dev.dsf.fhir.authorization.AuthorizationRule;
import dev.dsf.fhir.dao.StructureDefinitionDao;
@@ -53,24 +56,41 @@ public StructureDefinitionServiceSecure(StructureDefinitionService delegate, Str
@Override
public Response postSnapshotNew(String snapshotPath, Parameters parameters, UriInfo uri, HttpHeaders headers)
{
- return delegate.postSnapshotNew(snapshotPath, parameters, uri, headers);
+ Response response = delegate.postSnapshotNew(snapshotPath, parameters, uri, headers);
+
+ ParametersParameterComponent urlParam = parameters.getParameter("url");
+ Type urlType = urlParam == null ? null : urlParam.getValue();
+
+ ParametersParameterComponent resourceParam = parameters.getParameter("resource");
+ Resource resource = resourceParam == null ? null : resourceParam.getResource();
+
+ if (urlType != null && resource == null)
+ return checkRead(response);
+ else
+ return response;
}
@Override
public Response getSnapshotNew(String snapshotPath, UriInfo uri, HttpHeaders headers)
{
- return delegate.getSnapshotNew(snapshotPath, uri, headers);
+ Response response = delegate.getSnapshotNew(snapshotPath, uri, headers);
+
+ return checkRead(response);
}
@Override
public Response postSnapshotExisting(String snapshotPath, String id, UriInfo uri, HttpHeaders headers)
{
- return delegate.postSnapshotExisting(snapshotPath, id, uri, headers);
+ Response response = delegate.postSnapshotExisting(snapshotPath, id, uri, headers);
+
+ return checkRead(response);
}
@Override
public Response getSnapshotExisting(String snapshotPath, String id, UriInfo uri, HttpHeaders headers)
{
- return delegate.getSnapshotExisting(snapshotPath, id, uri, headers);
+ Response response = delegate.getSnapshotExisting(snapshotPath, id, uri, headers);
+
+ return checkRead(response);
}
}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractAuthorizationRuleTest.java
new file mode 100644
index 000000000..e454d8e13
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractAuthorizationRuleTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.sql.Connection;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+import org.hl7.fhir.r4.model.Resource;
+import org.junit.Before;
+import org.junit.Test;
+
+import dev.dsf.common.auth.conf.DsfRole;
+import dev.dsf.common.auth.conf.Identity;
+import dev.dsf.fhir.authentication.FhirServerRoleImpl;
+import dev.dsf.fhir.authentication.OrganizationProvider;
+import dev.dsf.fhir.authorization.read.ReadAccessHelper;
+import dev.dsf.fhir.dao.provider.DaoProvider;
+import dev.dsf.fhir.help.ParameterConverter;
+import dev.dsf.fhir.service.ReferenceResolver;
+
+public abstract class AbstractAuthorizationRuleTest
+{
+ protected static final String SERVER_BASE = "https://dsf.test/fhir";
+ protected static final UUID RESOURCE_UUID = UUID.fromString("d1e2f3a4-b5c6-4d7e-8f90-123456789abc");
+
+ protected DaoProvider daoProvider;
+ protected ReferenceResolver referenceResolver;
+ protected OrganizationProvider organizationProvider;
+ protected ReadAccessHelper readAccessHelper;
+ protected ParameterConverter parameterConverter;
+
+ protected AbstractAuthorizationRule rule;
+
+ @Before
+ public void setUpBase()
+ {
+ daoProvider = mock(DaoProvider.class);
+ referenceResolver = mock(ReferenceResolver.class);
+ organizationProvider = mock(OrganizationProvider.class);
+ readAccessHelper = mock(ReadAccessHelper.class);
+ parameterConverter = mock(ParameterConverter.class);
+ lenient().when(parameterConverter.toUuid(any(), any())).thenReturn(RESOURCE_UUID);
+
+ rule = createRule();
+ }
+
+ protected abstract AbstractAuthorizationRule createRule();
+
+ protected abstract Class expectedResourceType();
+
+ protected abstract R newResource();
+
+ protected static Identity identity(boolean localIdentity, boolean hasRole)
+ {
+ return identity(localIdentity, _ -> hasRole);
+ }
+
+ protected static Identity identity(boolean localIdentity, Predicate hasRole)
+ {
+ Identity identity = mock(Identity.class);
+ lenient().when(identity.isLocalIdentity()).thenReturn(localIdentity);
+ lenient().when(identity.hasDsfRole(any())).then(invocation -> hasRole.test(invocation.getArgument(0)));
+
+ return identity;
+ }
+
+ @Test
+ public void getResourceTypeMatchesExpected()
+ {
+ assertEquals(expectedResourceType(), rule.getResourceType());
+ }
+
+ @Test
+ public void afterPropertiesSetSucceedsWithAllDependencies() throws Exception
+ {
+ rule.afterPropertiesSet();
+ }
+
+ @Test
+ public void searchAllowedWhenIdentityHasSearchRole()
+ {
+ assertTrue(rule.reasonSearchAllowed(identity(true, true)).isPresent());
+ }
+
+ @Test
+ public void searchDeniedWhenIdentityHasNoSearchRole()
+ {
+ assertTrue(rule.reasonSearchAllowed(identity(true, false)).isEmpty());
+ }
+
+ @Test
+ public void historyAllowedWhenIdentityHasHistoryRole()
+ {
+ assertTrue(rule.reasonHistoryAllowed(identity(true, true)).isPresent());
+ }
+
+ @Test
+ public void historyDeniedWhenIdentityHasNoHistoryRole()
+ {
+ assertTrue(rule.reasonHistoryAllowed(identity(true, false)).isEmpty());
+ }
+
+ @Test
+ public void websocketAllowedForLocalIdentityWithWebsocketRole() throws Exception
+ {
+ when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class));
+
+ assertTrue(rule.reasonWebsocketAllowed(identity(true, true), newResource()).isPresent());
+ }
+
+ @Test
+ public void websocketDeniedForNonLocalIdentity() throws Exception
+ {
+ when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class));
+
+ assertTrue(rule.reasonWebsocketAllowed(identity(false, true), newResource()).isEmpty());
+ }
+
+ @Test
+ public void websocketDeniedWithoutWebsocketRole() throws Exception
+ {
+ when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class));
+
+ assertTrue(rule.reasonWebsocketAllowed(identity(true, false), newResource()).isEmpty());
+ }
+
+ @Test
+ public void permanentDeleteDeniedForNonLocalIdentity()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonPermanentDeleteAllowed(connection, identity(false, true), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void permanentDeleteDeniedWithoutPermanentDeleteRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonPermanentDeleteAllowed(connection, identity(true, false), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed() throws Exception
+ {
+ when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class));
+
+ assertTrue(rule.reasonPermanentDeleteAllowed(identity(true, true), newResource()).isPresent());
+ }
+
+ @Test
+ public void permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndNoDeleteRoleDenied() throws Exception
+ {
+ when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class));
+
+ assertTrue(rule.reasonPermanentDeleteAllowed(identity(true, role ->
+ {
+ if (FhirServerRoleImpl.delete(expectedResourceType()).equals(role))
+ return false;
+ else
+ return true;
+ }), newResource()).isEmpty());
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractMetaTagAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractMetaTagAuthorizationRuleTest.java
new file mode 100644
index 000000000..4fe47fe7d
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractMetaTagAuthorizationRuleTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import java.sql.Connection;
+
+import org.hl7.fhir.r4.model.Resource;
+import org.junit.Test;
+
+public abstract class AbstractMetaTagAuthorizationRuleTest extends AbstractAuthorizationRuleTest
+{
+ @Test
+ public void createDeniedWhenIdentityNotLocal()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonCreateAllowed(connection, identity(false, true), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void createDeniedWhenIdentityHasNoCreateRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonCreateAllowed(connection, identity(true, false), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void readDeniedWhenIdentityHasNoReadRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonReadAllowed(connection, identity(true, false), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void updateDeniedWhenIdentityNotLocal()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonUpdateAllowed(connection, identity(false, true), newResource(), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void updateDeniedWhenIdentityHasNoUpdateRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonUpdateAllowed(connection, identity(true, false), newResource(), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteDeniedWhenIdentityNotLocal()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonDeleteAllowed(connection, identity(false, true), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteDeniedWhenIdentityHasNoDeleteRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonDeleteAllowed(connection, identity(true, false), newResource()).isEmpty());
+ verifyNoInteractions(connection);
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ActivityDefinitionAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ActivityDefinitionAuthorizationRuleTest.java
new file mode 100644
index 000000000..df925ad6b
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ActivityDefinitionAuthorizationRuleTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import static org.mockito.Mockito.mock;
+
+import org.hl7.fhir.r4.model.ActivityDefinition;
+
+import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper;
+
+public class ActivityDefinitionAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected ActivityDefinitionAuthorizationRule createRule()
+ {
+ return new ActivityDefinitionAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver,
+ organizationProvider, readAccessHelper, parameterConverter, mock(ProcessAuthorizationHelper.class));
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return ActivityDefinition.class;
+ }
+
+ @Override
+ protected ActivityDefinition newResource()
+ {
+ ActivityDefinition resource = new ActivityDefinition();
+ resource.setId("ActivityDefinition/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BinaryAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BinaryAuthorizationRuleTest.java
new file mode 100644
index 000000000..e48052941
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BinaryAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Binary;
+
+public class BinaryAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected BinaryAuthorizationRule createRule()
+ {
+ return new BinaryAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Binary.class;
+ }
+
+ @Override
+ protected Binary newResource()
+ {
+ Binary resource = new Binary();
+ resource.setId("Binary/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BundleAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BundleAuthorizationRuleTest.java
new file mode 100644
index 000000000..3e3ae3f2e
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BundleAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Bundle;
+
+public class BundleAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected BundleAuthorizationRule createRule()
+ {
+ return new BundleAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Bundle.class;
+ }
+
+ @Override
+ protected Bundle newResource()
+ {
+ Bundle resource = new Bundle();
+ resource.setId("Bundle/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/CodeSystemAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/CodeSystemAuthorizationRuleTest.java
new file mode 100644
index 000000000..1fa46bc54
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/CodeSystemAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.CodeSystem;
+
+public class CodeSystemAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected CodeSystemAuthorizationRule createRule()
+ {
+ return new CodeSystemAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return CodeSystem.class;
+ }
+
+ @Override
+ protected CodeSystem newResource()
+ {
+ CodeSystem resource = new CodeSystem();
+ resource.setId("CodeSystem/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/DocumentReferenceAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/DocumentReferenceAuthorizationRuleTest.java
new file mode 100644
index 000000000..0f21ac8d1
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/DocumentReferenceAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.DocumentReference;
+
+public class DocumentReferenceAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected DocumentReferenceAuthorizationRule createRule()
+ {
+ return new DocumentReferenceAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return DocumentReference.class;
+ }
+
+ @Override
+ protected DocumentReference newResource()
+ {
+ DocumentReference resource = new DocumentReference();
+ resource.setId("DocumentReference/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/EndpointAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/EndpointAuthorizationRuleTest.java
new file mode 100644
index 000000000..a01fa4b9c
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/EndpointAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Endpoint;
+
+public class EndpointAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected EndpointAuthorizationRule createRule()
+ {
+ return new EndpointAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Endpoint.class;
+ }
+
+ @Override
+ protected Endpoint newResource()
+ {
+ Endpoint resource = new Endpoint();
+ resource.setId("Endpoint/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/GroupAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/GroupAuthorizationRuleTest.java
new file mode 100644
index 000000000..253cad708
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/GroupAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Group;
+
+public class GroupAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected GroupAuthorizationRule createRule()
+ {
+ return new GroupAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Group.class;
+ }
+
+ @Override
+ protected Group newResource()
+ {
+ Group resource = new Group();
+ resource.setId("Group/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/HealthcareServiceAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/HealthcareServiceAuthorizationRuleTest.java
new file mode 100644
index 000000000..35349aaea
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/HealthcareServiceAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.HealthcareService;
+
+public class HealthcareServiceAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected HealthcareServiceAuthorizationRule createRule()
+ {
+ return new HealthcareServiceAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return HealthcareService.class;
+ }
+
+ @Override
+ protected HealthcareService newResource()
+ {
+ HealthcareService resource = new HealthcareService();
+ resource.setId("HealthcareService/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LibraryAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LibraryAuthorizationRuleTest.java
new file mode 100644
index 000000000..513e03a51
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LibraryAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Library;
+
+public class LibraryAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected LibraryAuthorizationRule createRule()
+ {
+ return new LibraryAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Library.class;
+ }
+
+ @Override
+ protected Library newResource()
+ {
+ Library resource = new Library();
+ resource.setId("Library/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LocationAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LocationAuthorizationRuleTest.java
new file mode 100644
index 000000000..485b25f7c
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LocationAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Location;
+
+public class LocationAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected LocationAuthorizationRule createRule()
+ {
+ return new LocationAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Location.class;
+ }
+
+ @Override
+ protected Location newResource()
+ {
+ Location resource = new Location();
+ resource.setId("Location/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureAuthorizationRuleTest.java
new file mode 100644
index 000000000..86d1a33ce
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Measure;
+
+public class MeasureAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected MeasureAuthorizationRule createRule()
+ {
+ return new MeasureAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Measure.class;
+ }
+
+ @Override
+ protected Measure newResource()
+ {
+ Measure resource = new Measure();
+ resource.setId("Measure/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureReportAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureReportAuthorizationRuleTest.java
new file mode 100644
index 000000000..734702b96
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureReportAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.MeasureReport;
+
+public class MeasureReportAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected MeasureReportAuthorizationRule createRule()
+ {
+ return new MeasureReportAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return MeasureReport.class;
+ }
+
+ @Override
+ protected MeasureReport newResource()
+ {
+ MeasureReport resource = new MeasureReport();
+ resource.setId("MeasureReport/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/NamingSystemAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/NamingSystemAuthorizationRuleTest.java
new file mode 100644
index 000000000..c4120876a
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/NamingSystemAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.NamingSystem;
+
+public class NamingSystemAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected NamingSystemAuthorizationRule createRule()
+ {
+ return new NamingSystemAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return NamingSystem.class;
+ }
+
+ @Override
+ protected NamingSystem newResource()
+ {
+ NamingSystem resource = new NamingSystem();
+ resource.setId("NamingSystem/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRuleTest.java
new file mode 100644
index 000000000..34ef4912b
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRuleTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+
+public class OrganizationAffiliationAuthorizationRuleTest
+ extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected OrganizationAffiliationAuthorizationRule createRule()
+ {
+ return new OrganizationAffiliationAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver,
+ organizationProvider, readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return OrganizationAffiliation.class;
+ }
+
+ @Override
+ protected OrganizationAffiliation newResource()
+ {
+ OrganizationAffiliation resource = new OrganizationAffiliation();
+ resource.setId("OrganizationAffiliation/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAuthorizationRuleTest.java
new file mode 100644
index 000000000..ef9efb435
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Organization;
+
+public class OrganizationAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected OrganizationAuthorizationRule createRule()
+ {
+ return new OrganizationAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Organization.class;
+ }
+
+ @Override
+ protected Organization newResource()
+ {
+ Organization resource = new Organization();
+ resource.setId("Organization/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PatientAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PatientAuthorizationRuleTest.java
new file mode 100644
index 000000000..48aad4287
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PatientAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Patient;
+
+public class PatientAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected PatientAuthorizationRule createRule()
+ {
+ return new PatientAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Patient.class;
+ }
+
+ @Override
+ protected Patient newResource()
+ {
+ Patient resource = new Patient();
+ resource.setId("Patient/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerAuthorizationRuleTest.java
new file mode 100644
index 000000000..4ca8f3c34
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Practitioner;
+
+public class PractitionerAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected PractitionerAuthorizationRule createRule()
+ {
+ return new PractitionerAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Practitioner.class;
+ }
+
+ @Override
+ protected Practitioner newResource()
+ {
+ Practitioner resource = new Practitioner();
+ resource.setId("Practitioner/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerRoleAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerRoleAuthorizationRuleTest.java
new file mode 100644
index 000000000..b392646e1
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerRoleAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.PractitionerRole;
+
+public class PractitionerRoleAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected PractitionerRoleAuthorizationRule createRule()
+ {
+ return new PractitionerRoleAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return PractitionerRole.class;
+ }
+
+ @Override
+ protected PractitionerRole newResource()
+ {
+ PractitionerRole resource = new PractitionerRole();
+ resource.setId("PractitionerRole/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ProvenanceAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ProvenanceAuthorizationRuleTest.java
new file mode 100644
index 000000000..883660dcb
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ProvenanceAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Provenance;
+
+public class ProvenanceAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected ProvenanceAuthorizationRule createRule()
+ {
+ return new ProvenanceAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Provenance.class;
+ }
+
+ @Override
+ protected Provenance newResource()
+ {
+ Provenance resource = new Provenance();
+ resource.setId("Provenance/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireAuthorizationRuleTest.java
new file mode 100644
index 000000000..6aac8eacc
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Questionnaire;
+
+public class QuestionnaireAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected QuestionnaireAuthorizationRule createRule()
+ {
+ return new QuestionnaireAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Questionnaire.class;
+ }
+
+ @Override
+ protected Questionnaire newResource()
+ {
+ Questionnaire resource = new Questionnaire();
+ resource.setId("Questionnaire/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRuleTest.java
new file mode 100644
index 000000000..0d0c50092
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRuleTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.sql.Connection;
+
+import org.hl7.fhir.r4.model.QuestionnaireResponse;
+import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import dev.dsf.common.auth.conf.OrganizationIdentity;
+
+public class QuestionnaireResponseAuthorizationRuleTest extends AbstractAuthorizationRuleTest
+{
+ @Override
+ protected QuestionnaireResponseAuthorizationRule createRule()
+ {
+ return new QuestionnaireResponseAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver,
+ organizationProvider, readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return QuestionnaireResponse.class;
+ }
+
+ @Override
+ protected QuestionnaireResponse newResource()
+ {
+ return response(QuestionnaireResponseStatus.INPROGRESS);
+ }
+
+ private static QuestionnaireResponse response(QuestionnaireResponseStatus status)
+ {
+ QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse();
+ questionnaireResponse.setId("QuestionnaireResponse/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ questionnaireResponse.setStatus(status);
+ return questionnaireResponse;
+ }
+
+ private static OrganizationIdentity localOrganizationIdentity()
+ {
+ OrganizationIdentity identity = mock(OrganizationIdentity.class);
+ when(identity.isLocalIdentity()).thenReturn(true);
+ when(identity.hasDsfRole(any())).thenReturn(true);
+ return identity;
+ }
+
+ @Test
+ public void createDeniedWhenIdentityHasNoCreateRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonCreateAllowed(connection, identity(true, false),
+ response(QuestionnaireResponseStatus.INPROGRESS)).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void createDeniedWhenIdentityNotLocalOrganizationOrDsfAdmin()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule
+ .reasonCreateAllowed(connection, identity(true, true), response(QuestionnaireResponseStatus.INPROGRESS))
+ .isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void createDeniedWhenStatusNotInProgress()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonCreateAllowed(connection, localOrganizationIdentity(),
+ response(QuestionnaireResponseStatus.STOPPED)).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void readAllowedForLocalOrganization()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonReadAllowed(connection, localOrganizationIdentity(),
+ response(QuestionnaireResponseStatus.COMPLETED)).isPresent());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void readDeniedWhenIdentityHasNoReadRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule
+ .reasonReadAllowed(connection, identity(true, false), response(QuestionnaireResponseStatus.COMPLETED))
+ .isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void readDeniedForIdentityNotLocalOrganizationAndNotAuthorizedPractitioner()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule
+ .reasonReadAllowed(connection, identity(true, true), response(QuestionnaireResponseStatus.COMPLETED))
+ .isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void updateDeniedWhenIdentityHasNoUpdateRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonUpdateAllowed(connection, identity(true, false),
+ response(QuestionnaireResponseStatus.INPROGRESS), response(QuestionnaireResponseStatus.COMPLETED))
+ .isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void updateDeniedWhenIdentityNotLocal()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonUpdateAllowed(connection, identity(false, true),
+ response(QuestionnaireResponseStatus.INPROGRESS), response(QuestionnaireResponseStatus.COMPLETED))
+ .isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteAllowedForLocalOrganization()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonDeleteAllowed(connection, localOrganizationIdentity(),
+ response(QuestionnaireResponseStatus.COMPLETED)).isPresent());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteDeniedWhenIdentityHasNoDeleteRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule
+ .reasonDeleteAllowed(connection, identity(true, false), response(QuestionnaireResponseStatus.COMPLETED))
+ .isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteDeniedWhenIdentityNotLocalOrganizationOrDsfAdmin()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule
+ .reasonDeleteAllowed(connection, identity(true, true), response(QuestionnaireResponseStatus.COMPLETED))
+ .isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ @Ignore
+ @Override
+ public void permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed() throws Exception
+ {
+ super.permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed();
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ResearchStudyAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ResearchStudyAuthorizationRuleTest.java
new file mode 100644
index 000000000..6fcd2473f
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ResearchStudyAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.ResearchStudy;
+
+public class ResearchStudyAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected ResearchStudyAuthorizationRule createRule()
+ {
+ return new ResearchStudyAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return ResearchStudy.class;
+ }
+
+ @Override
+ protected ResearchStudy newResource()
+ {
+ ResearchStudy resource = new ResearchStudy();
+ resource.setId("ResearchStudy/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRuleTest.java
new file mode 100644
index 000000000..4bb7397b3
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.StructureDefinition;
+
+public class StructureDefinitionAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected StructureDefinitionAuthorizationRule createRule()
+ {
+ return new StructureDefinitionAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver,
+ organizationProvider, readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return StructureDefinition.class;
+ }
+
+ @Override
+ protected StructureDefinition newResource()
+ {
+ StructureDefinition resource = new StructureDefinition();
+ resource.setId("StructureDefinition/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/SubscriptionAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/SubscriptionAuthorizationRuleTest.java
new file mode 100644
index 000000000..64846e995
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/SubscriptionAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.Subscription;
+
+public class SubscriptionAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected SubscriptionAuthorizationRule createRule()
+ {
+ return new SubscriptionAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Subscription.class;
+ }
+
+ @Override
+ protected Subscription newResource()
+ {
+ Subscription resource = new Subscription();
+ resource.setId("Subscription/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/TaskAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/TaskAuthorizationRuleTest.java
new file mode 100644
index 000000000..c0abdce2d
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/TaskAuthorizationRuleTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.sql.Connection;
+
+import org.hl7.fhir.r4.model.Task;
+import org.hl7.fhir.r4.model.Task.TaskStatus;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import ca.uhn.fhir.context.FhirContext;
+import dev.dsf.common.auth.conf.OrganizationIdentity;
+import dev.dsf.fhir.authentication.EndpointProvider;
+import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper;
+
+public class TaskAuthorizationRuleTest extends AbstractAuthorizationRuleTest
+{
+ private static final FhirContext FHIR_CONTEXT = FhirContext.forR4();
+
+ private final ProcessAuthorizationHelper processAuthorizationHelper = mock(ProcessAuthorizationHelper.class);
+ private final EndpointProvider endpointProvider = mock(EndpointProvider.class);
+
+ @Override
+ protected TaskAuthorizationRule createRule()
+ {
+ return newRule(processAuthorizationHelper, endpointProvider);
+ }
+
+ private TaskAuthorizationRule newRule(ProcessAuthorizationHelper processAuthorizationHelper,
+ EndpointProvider endpointProvider)
+ {
+ return new TaskAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter, processAuthorizationHelper, FHIR_CONTEXT, endpointProvider);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return Task.class;
+ }
+
+ @Override
+ protected Task newResource()
+ {
+ return task(TaskStatus.DRAFT);
+ }
+
+ private static Task task(TaskStatus status)
+ {
+ Task task = new Task();
+ task.setId("Task/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ task.setStatus(status);
+ return task;
+ }
+
+ private static OrganizationIdentity localOrganizationIdentity()
+ {
+ OrganizationIdentity identity = mock(OrganizationIdentity.class);
+ when(identity.isLocalIdentity()).thenReturn(true);
+ when(identity.hasDsfRole(any())).thenReturn(true);
+ return identity;
+ }
+
+ @Test
+ public void afterPropertiesSetThrowsWhenProcessAuthorizationHelperNull()
+ {
+ TaskAuthorizationRule ruleWithNull = newRule(null, endpointProvider);
+ assertThrows(NullPointerException.class, ruleWithNull::afterPropertiesSet);
+ }
+
+ @Test
+ public void afterPropertiesSetThrowsWhenEndpointProviderNull()
+ {
+ TaskAuthorizationRule ruleWithNull = newRule(processAuthorizationHelper, null);
+ assertThrows(NullPointerException.class, ruleWithNull::afterPropertiesSet);
+ }
+
+ @Test
+ public void createDeniedWhenIdentityHasNoCreateRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonCreateAllowed(connection, identity(true, false), task(TaskStatus.REQUESTED)).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void createDeniedWhenTaskStatusNotDraftOrRequested()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonCreateAllowed(connection, identity(true, true), task(TaskStatus.COMPLETED)).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void createDraftDeniedWhenIdentityNotLocalOrganizationOrDsfAdmin()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonCreateAllowed(connection, identity(true, true), task(TaskStatus.DRAFT)).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteAllowedForLocalOrganizationWhenTaskStatusDraft()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(
+ rule.reasonDeleteAllowed(connection, localOrganizationIdentity(), task(TaskStatus.DRAFT)).isPresent());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteDeniedWhenIdentityHasNoDeleteRole()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonDeleteAllowed(connection, identity(true, false), task(TaskStatus.DRAFT)).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteDeniedWhenIdentityNotLocalOrganizationOrDsfAdmin()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonDeleteAllowed(connection, identity(true, true), task(TaskStatus.DRAFT)).isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ public void deleteDeniedWhenTaskStatusNotDraft()
+ {
+ Connection connection = mock(Connection.class);
+
+ assertTrue(rule.reasonDeleteAllowed(connection, localOrganizationIdentity(), task(TaskStatus.REQUESTED))
+ .isEmpty());
+ verifyNoInteractions(connection);
+ }
+
+ @Test
+ @Ignore
+ @Override
+ public void permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed() throws Exception
+ {
+ super.permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed();
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ValueSetAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ValueSetAuthorizationRuleTest.java
new file mode 100644
index 000000000..165eca1ad
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ValueSetAuthorizationRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization;
+
+import org.hl7.fhir.r4.model.ValueSet;
+
+public class ValueSetAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest
+{
+ @Override
+ protected ValueSetAuthorizationRule createRule()
+ {
+ return new ValueSetAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider,
+ readAccessHelper, parameterConverter);
+ }
+
+ @Override
+ protected Class expectedResourceType()
+ {
+ return ValueSet.class;
+ }
+
+ @Override
+ protected ValueSet newResource()
+ {
+ ValueSet resource = new ValueSet();
+ resource.setId("ValueSet/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1");
+ return resource;
+ }
+}
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/HistoryIdentityFilterTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/HistoryIdentityFilterTest.java
new file mode 100644
index 000000000..af9ecbea9
--- /dev/null
+++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/HistoryIdentityFilterTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.history.filter;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.util.List;
+import java.util.function.Function;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import dev.dsf.common.auth.conf.Identity;
+
+@RunWith(Parameterized.class)
+public class HistoryIdentityFilterTest
+{
+ @Parameters(name = "{1}")
+ public static List