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 data() + { + return List.of(row(ActivityDefinitionHistoryIdentityFilter::new, "ActivityDefinition"), + row(BinaryHistoryIdentityFilter::new, "Binary"), row(BundleHistoryIdentityFilter::new, "Bundle"), + row(CodeSystemHistoryIdentityFilter::new, "CodeSystem"), + row(DocumentReferenceHistoryIdentityFilter::new, "DocumentReference"), + row(EndpointHistoryIdentityFilter::new, "Endpoint"), row(GroupHistoryIdentityFilter::new, "Group"), + row(HealthcareServiceHistoryIdentityFilter::new, "HealthcareService"), + row(LibraryHistoryIdentityFilter::new, "Library"), row(LocationHistoryIdentityFilter::new, "Location"), + row(MeasureHistoryIdentityFilter::new, "Measure"), + row(MeasureReportHistoryIdentityFilter::new, "MeasureReport"), + row(NamingSystemHistoryIdentityFilter::new, "NamingSystem"), + row(OrganizationAffiliationHistoryIdentityFilter::new, "OrganizationAffiliation"), + row(OrganizationHistoryIdentityFilter::new, "Organization"), + row(PatientHistoryIdentityFilter::new, "Patient"), + row(PractitionerHistoryIdentityFilter::new, "Practitioner"), + row(PractitionerRoleHistoryIdentityFilter::new, "PractitionerRole"), + row(ProvenanceHistoryIdentityFilter::new, "Provenance"), + row(QuestionnaireHistoryIdentityFilter::new, "Questionnaire"), + row(QuestionnaireResponseHistoryIdentityFilter::new, "QuestionnaireResponse"), + row(ResearchStudyHistoryIdentityFilter::new, "ResearchStudy"), + row(StructureDefinitionHistoryIdentityFilter::new, "StructureDefinition"), + row(SubscriptionHistoryIdentityFilter::new, "Subscription"), + row(TaskHistoryIdentityFilter::new, "Task"), row(ValueSetHistoryIdentityFilter::new, "ValueSet")); + } + + private static Object[] row(Function factory, String resourceType) + { + return new Object[] { factory, resourceType }; + } + + @Parameter(0) + public Function filterFactory; + + @Parameter(1) + public String resourceType; + + @Test + public void filterQueryScopesToResourceType() + { + HistoryIdentityFilter filter = filterFactory.apply(mock(Identity.class)); + + String query = filter.getFilterQuery(); + assertNotNull(query); + assertTrue("filter query must scope to type '" + resourceType + "' but was: " + query, + query.contains("type = '" + resourceType + "'")); + } + + @Test + public void filterQueryIsDefinedAndParameterCountNonNegative() + { + HistoryIdentityFilter filter = filterFactory.apply(mock(Identity.class)); + + assertTrue(filter.isDefined()); + assertTrue(filter.getSqlParameterCount() >= 0); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/TaskHistoryIdentityFilterTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/TaskHistoryIdentityFilterTest.java new file mode 100644 index 000000000..67cdd70c0 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/TaskHistoryIdentityFilterTest.java @@ -0,0 +1,74 @@ +/* + * 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.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import dev.dsf.common.auth.conf.Identity; +import dev.dsf.common.auth.conf.OrganizationIdentity; + +public class TaskHistoryIdentityFilterTest +{ + @Test + public void identityWithoutRoleSeesNothing() + { + TaskHistoryIdentityFilter filter = new TaskHistoryIdentityFilter(mock(Identity.class)); + + String query = filter.getFilterQuery(); + assertTrue(query.contains("type = 'Task'")); + assertTrue(query.contains("FALSE")); + assertEquals(0, filter.getSqlParameterCount()); + } + + @Test + public void localOrganizationSeesTasksWhereItIsRecipient() + { + OrganizationIdentity identity = mock(OrganizationIdentity.class); + when(identity.hasDsfRole(any())).thenReturn(true); + when(identity.isLocalIdentity()).thenReturn(true); + + TaskHistoryIdentityFilter filter = new TaskHistoryIdentityFilter(identity); + + String query = filter.getFilterQuery(); + assertTrue(query.contains("type = 'Task'")); + assertTrue(query.contains("recipient")); + assertFalse(query.contains("FALSE")); + assertEquals(1, filter.getSqlParameterCount()); + } + + @Test + public void remoteOrganizationSeesTasksWhereItIsRequester() + { + OrganizationIdentity identity = mock(OrganizationIdentity.class); + when(identity.hasDsfRole(any())).thenReturn(true); + when(identity.isLocalIdentity()).thenReturn(false); + + TaskHistoryIdentityFilter filter = new TaskHistoryIdentityFilter(identity); + + String query = filter.getFilterQuery(); + assertTrue(query.contains("type = 'Task'")); + assertTrue(query.contains("requester")); + assertFalse(query.contains("FALSE")); + assertEquals(1, filter.getSqlParameterCount()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java index 3652d13e5..556f8e2b1 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java @@ -21,9 +21,14 @@ import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; import org.hl7.fhir.r4.model.Bundle.SearchEntryMode; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.Extension; @@ -271,4 +276,95 @@ public void testStrictSearchWithUnsupportedRevIncludeParameter() throws Exceptio expectBadRequest(() -> getWebserviceClient().searchWithStrictHandling(Organization.class, Map.of("_revinclude", List.of("Endpoint:foo")))); } + + private Organization prepareForTestIllegalUpdate() + { + Bundle bundle = getWebserviceClient().search(Organization.class, + Map.of("identifier", List.of("http://dsf.dev/sid/organization-identifier|External_Test_Organization"))); + assertNotNull(bundle); + assertNotNull(bundle.getEntry()); + assertEquals(1, bundle.getEntry().size()); + assertNotNull(bundle.getEntry().get(0).getResource()); + assertTrue(bundle.getEntry().get(0).getResource() instanceof Organization); + + Organization o = (Organization) bundle.getEntry().get(0).getResource(); + o.getIdentifierFirstRep().setValue("Test_Organization2"); + o.getExtensionByUrl("http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint") + .setValue(new StringType(IntStream.range(0, 128).mapToObj(_ -> "f").collect(Collectors.joining()))); + return o; + } + + @Test + public void testIllegalUpdate() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().update(o)); + } + + @Test + public void testIllegalConditionalUpdateWithId() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().updateConditionaly(o, + Map.of("_id", List.of(o.getIdElement().getIdPart())))); + } + + @Test + public void testIllegalConditionalUpdateWithoutId() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().updateConditionaly(o, + Map.of("_id", List.of(o.getIdElement().getIdPart())))); + } + + @Test + public void testIllegalUpdateViaBundle() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + BundleEntryComponent entry = b.addEntry() + .setFullUrl(o.getIdElement().withServerBase(getBaseUrl(), "Organization").toVersionless().getValue()); + entry.setResource(o).getRequest().setMethod(HTTPVerb.PUT) + .setUrl("Organization/" + o.getIdElement().getIdPart()); + + Bundle resultBundle = getWebserviceClient().postBundle(b); + assertNotNull(resultBundle); + assertNotNull(resultBundle.getEntry()); + assertEquals(1, resultBundle.getEntry().size()); + assertNotNull(resultBundle.getEntry().get(0)); + assertNotNull(resultBundle.getEntry().get(0).getResponse()); + assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); + assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); + } + + @Test + public void testIllegalConditionalUpdateViaBundle() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + BundleEntryComponent entry = b.addEntry().setFullUrl("urn:uuid:" + UUID.randomUUID().toString()); + entry.setResource(o).getRequest().setMethod(HTTPVerb.PUT) + .setUrl("Organization?_id=" + o.getIdElement().getIdPart()); + + o.setIdElement(null); + o.getMeta().setVersionId(null); + + Bundle resultBundle = getWebserviceClient().postBundle(b); + assertNotNull(resultBundle); + assertNotNull(resultBundle.getEntry()); + assertEquals(1, resultBundle.getEntry().size()); + assertNotNull(resultBundle.getEntry().get(0)); + assertNotNull(resultBundle.getEntry().get(0).getResponse()); + assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); + assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); + } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/StructureDefinitionIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/StructureDefinitionIntegrationTest.java index e85b540c8..ef7c02f2e 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/StructureDefinitionIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/StructureDefinitionIntegrationTest.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.function.Function; import org.hl7.fhir.r4.model.Bundle; @@ -49,13 +50,23 @@ public class StructureDefinitionIntegrationTest extends AbstractIntegrationTest { private static final Path PROFILE_FOLDER = Paths.get("src/test/resources/integration/structuredefinition"); - private void testCreateWithoutSnapshot(Function createOp) throws Exception + private StructureDefinition testCreateWithoutSnapshot(Function createOp) + throws Exception + { + return testCreateWithoutSnapshot(createOp, _ -> + {}); + } + + private StructureDefinition testCreateWithoutSnapshot(Function createOp, + Consumer profileModifier) throws Exception { EventManager eventManager = getSpringWebApplicationContext().getBean(EventManager.class); List events = new ArrayList<>(); eventManager.addHandler(events::add); StructureDefinition profile = readProfile(PROFILE_FOLDER.resolve("dsf-task-test.xml")); + profileModifier.accept(profile); + StructureDefinition created = createOp.apply(profile); assertNotNull(created); assertTrue(created.hasIdElement()); @@ -76,15 +87,27 @@ private void testCreateWithoutSnapshot(Function createOp) throws Exception + private StructureDefinition testCreateWithSnapshot(Function createOp) + throws Exception + { + return testCreateWithSnapshot(createOp, _ -> + {}); + } + + private StructureDefinition testCreateWithSnapshot(Function createOp, + Consumer profileModifier) throws Exception { EventManager eventManager = getSpringWebApplicationContext().getBean(EventManager.class); List events = new ArrayList<>(); eventManager.addHandler(events::add); StructureDefinition profile = readProfile(PROFILE_FOLDER.resolve("dsf-task-test-snapshot.xml")); + profileModifier.accept(profile); + StructureDefinition created = createOp.apply(profile); assertNotNull(created); assertTrue(created.hasIdElement()); @@ -105,6 +128,8 @@ private void testCreateWithSnapshot(Function getExternalWebserviceClient().read(StructureDefinition.class, + created.getIdElement().getIdPart())); + + expectForbidden( + () -> getExternalWebserviceClient().generateSnapshot(created.getUrl() + "|" + created.getVersion())); + + expectForbidden(() -> getExternalWebserviceClient().getSnapshot(created.getIdElement().getIdPart())); + } + + @Test + public void testGetSnapshotAllowedWithoutSnapshotInCreatedResource() throws Exception + { + StructureDefinition created = testCreateWithoutSnapshot(getWebserviceClient()::create, + getReadAccessHelper()::addLocal); + assertFalse(created.hasSnapshot()); + + StructureDefinition read = getWebserviceClient().read(StructureDefinition.class, + created.getIdElement().getIdPart()); + assertNotNull(read); + assertFalse(read.hasSnapshot()); + + StructureDefinition snapshot1 = getWebserviceClient() + .generateSnapshot(created.getUrl() + "|" + created.getVersion()); + assertNotNull(snapshot1); + assertTrue(snapshot1.hasSnapshot()); + + StructureDefinition snapshot2 = getWebserviceClient().getSnapshot(created.getIdElement().getIdPart()); + assertNotNull(snapshot2); + assertTrue(snapshot2.hasSnapshot()); + } + + @Test + public void testGenerateSnapshotForResource() throws Exception + { + StructureDefinition profile = readProfile(PROFILE_FOLDER.resolve("dsf-task-test.xml")); + profile.getMeta().setTag(null); + + StructureDefinition snapshot1 = getWebserviceClient().generateSnapshot(profile); + assertNotNull(snapshot1); + assertTrue(snapshot1.hasSnapshot()); + + StructureDefinition snapshot2 = getExternalWebserviceClient().generateSnapshot(profile); + assertNotNull(snapshot2); + assertTrue(snapshot2.hasSnapshot()); + + StructureDefinition profileWithSnapshot = readProfile(PROFILE_FOLDER.resolve("dsf-task-test-snapshot.xml")); + profileWithSnapshot.getMeta().setTag(null); + + StructureDefinition snapshot3 = getWebserviceClient().generateSnapshot(profileWithSnapshot); + assertNotNull(snapshot3); + assertTrue(snapshot3.hasSnapshot()); + + StructureDefinition snapshot4 = getExternalWebserviceClient().generateSnapshot(profileWithSnapshot); + assertNotNull(snapshot4); + assertTrue(snapshot4.hasSnapshot()); + } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java index 0678b9c37..83853b7cf 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java @@ -56,6 +56,7 @@ import org.hl7.fhir.r4.model.Task.TaskIntent; import org.hl7.fhir.r4.model.Task.TaskRestrictionComponent; import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.hl7.fhir.r4.model.Type; import org.hl7.fhir.r4.model.ValueSet; import org.junit.Test; import org.slf4j.Logger; @@ -2381,4 +2382,144 @@ public void testSerachTaskWithPractitionerUserByRequester() throws Exception assertEquals(Task.class, entry.getResource().getClass()); assertEquals(createdTask.getIdElement().getIdPart(), entry.getResource().getIdElement().getIdPart()); } + + private Task prepareForTestIllegalUpdate() throws SQLException, IOException + { + OrganizationDao orgDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization o = new Organization(); + o.getIdentifierFirstRep().setSystem("http://dsf.dev/sid/organization-identifier").setValue("Foo"); + orgDao.create(o); + + Task read = readTestTaskBinary("External_Test_Organization", "Test_Organization"); + Type value = read.getInput().getLast().getValue(); + assertNotNull(value); + assertTrue(value instanceof Reference); + Reference reference = (Reference) value; + reference.setReference("https://localhost:60010/fhir/Binary/941683ea-7670-4d1a-8e0d-75698c433204"); + + Task created = createTaskBinary(read, TaskStatus.REQUESTED, true); + created.setStatus(TaskStatus.INPROGRESS); + created.getRequester().getIdentifier().setValue("Foo"); + return created; + } + + @Test + public void testIllegalUpdate() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().update(created)); + } + + @Test + public void testIllegalConditionalUpdateWithId() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().updateConditionaly(created, + Map.of("_id", List.of(created.getIdElement().getIdPart())))); + } + + @Test + public void testIllegalConditionalUpdateWithoutId() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + String id = created.getIdElement().getIdPart(); + created.setIdElement(null); + created.getMeta().setVersionId(null); + + expectForbidden(() -> getWebserviceClient().updateConditionaly(created, Map.of("_id", List.of(id)))); + } + + @Test + public void testIllegalUpdateViaBundle() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + BundleEntryComponent entry = b.addEntry() + .setFullUrl(created.getIdElement().withServerBase(getBaseUrl(), "Task").toVersionless().getValue()); + entry.setResource(created).getRequest().setMethod(HTTPVerb.PUT) + .setUrl("Task/" + created.getIdElement().getIdPart()); + + Bundle resultBundle = getWebserviceClient().postBundle(b); + assertNotNull(resultBundle); + assertNotNull(resultBundle.getEntry()); + assertEquals(1, resultBundle.getEntry().size()); + assertNotNull(resultBundle.getEntry().get(0)); + assertNotNull(resultBundle.getEntry().get(0).getResponse()); + assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); + assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); + } + + @Test + public void testIllegalConditionalUpdateViaBundle() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + BundleEntryComponent entry = b.addEntry().setFullUrl("urn:uuid:" + UUID.randomUUID().toString()); + entry.setResource(created).getRequest().setMethod(HTTPVerb.PUT) + .setUrl("Task?_id=" + created.getIdElement().getIdPart()); + + created.setIdElement(null); + created.getMeta().setVersionId(null); + + Bundle resultBundle = getWebserviceClient().postBundle(b); + assertNotNull(resultBundle); + assertNotNull(resultBundle.getEntry()); + assertEquals(1, resultBundle.getEntry().size()); + assertNotNull(resultBundle.getEntry().get(0)); + assertNotNull(resultBundle.getEntry().get(0).getResponse()); + assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); + assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); + } + + @Test + public void testDeleteDraftTaskAllowedLocalOrganization() throws Exception + { + ActivityDefinition ad = readActivityDefinition("dsf-test-activity-definition14-1.0.xml"); + getWebserviceClient().create(ad); + + StructureDefinition profile = readTestTaskProfile(); + getWebserviceClient().create(profile); + + Task t = readTestTask("Test_Organization", null, "Test_Organization"); + t.addIdentifier().setSystem("http://dsf.dev/sid/task-identifier").setValue("delete-allowed"); + t.setStatus(TaskStatus.DRAFT); + Task createdT = getWebserviceClient().create(t); + assertNotNull(createdT); + String id = createdT.getIdElement().getIdPart(); + assertNotNull(id); + + getWebserviceClient().delete(Task.class, id); + + Bundle searchResult = getWebserviceClient().search(Task.class, Map.of()); + assertNotNull(searchResult); + assertEquals(0, searchResult.getTotal()); + } + + @Test + public void testDeleteDraftTaskForbiddenExternalOrganization() throws Exception + { + ActivityDefinition ad = readActivityDefinition("dsf-test-activity-definition14-1.0.xml"); + getWebserviceClient().create(ad); + + StructureDefinition profile = readTestTaskProfile(); + getWebserviceClient().create(profile); + + Task t = readTestTask("Test_Organization", null, "Test_Organization"); + t.addIdentifier().setSystem("http://dsf.dev/sid/task-identifier").setValue("delete-forbidden"); + t.setStatus(TaskStatus.DRAFT); + Task createdT = getWebserviceClient().create(t); + assertNotNull(createdT); + String id = createdT.getIdElement().getIdPart(); + assertNotNull(id); + + expectForbidden(() -> getExternalWebserviceClient().delete(Task.class, id)); + } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecureTest.java new file mode 100644 index 000000000..01b31e1ca --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecureTest.java @@ -0,0 +1,1163 @@ +/* + * 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.webservice.secure; + +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.annotation.ResourceDef; +import ca.uhn.fhir.validation.ResultSeverityEnum; +import ca.uhn.fhir.validation.SingleValidationMessage; +import ca.uhn.fhir.validation.ValidationResult; +import dev.dsf.common.auth.conf.Identity; +import dev.dsf.fhir.authentication.CurrentIdentityProvider; +import dev.dsf.fhir.authorization.AuthorizationRule; +import dev.dsf.fhir.dao.ResourceDao; +import dev.dsf.fhir.help.ExceptionHandler; +import dev.dsf.fhir.help.ParameterConverter; +import dev.dsf.fhir.help.ResponseGenerator; +import dev.dsf.fhir.search.PageAndCount; +import dev.dsf.fhir.search.PartialResult; +import dev.dsf.fhir.search.SearchQuery; +import dev.dsf.fhir.search.SearchQueryParameterError; +import dev.dsf.fhir.search.SearchQueryParameterError.SearchQueryParameterErrorType; +import dev.dsf.fhir.service.DefaultProfileProvider; +import dev.dsf.fhir.service.ReferenceCleaner; +import dev.dsf.fhir.service.ReferenceExtractor; +import dev.dsf.fhir.service.ReferenceResolver; +import dev.dsf.fhir.validation.ResourceValidator; +import dev.dsf.fhir.validation.ValidationRules; +import dev.dsf.fhir.webservice.specification.BasicResourceService; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; + +public abstract class AbstractResourceServiceSecureTest, D extends ResourceDao> +{ + @FunctionalInterface + public interface ResourceServiceSecureFactory, D extends ResourceDao> + { + S create(S delegate, String serverBase, ResponseGenerator responseGenerator, + ReferenceResolver referenceResolver, ReferenceCleaner referenceCleaner, + ReferenceExtractor referenceExtractor, D binaryDao, ExceptionHandler exceptionHandler, + ParameterConverter parameterConverter, AuthorizationRule authorizationRule, + ResourceValidator resourceValidator, ValidationRules validationRules, + DefaultProfileProvider defaultProfileProvider); + } + + protected static final String SERVER_BASE = "https://dsf.test/fhir"; + private static final String PERMANENT_DELETE_PATH = "$permanent-delete"; + + protected static final FhirContext FHIR_CONTEXT = FhirContext.forR4(); + + protected final Class resourceClass; + protected final Class serviceClass; + protected final Class daoClass; + protected final Supplier resouceSupplier; + protected final ResourceServiceSecureFactory resourceServiceSecureFactory; + + protected final ValidationRules validationRules = new ValidationRules(SERVER_BASE); + protected final ResponseGenerator responseGenerator = new ResponseGenerator(SERVER_BASE); + protected final ExceptionHandler exceptionHandler = new ExceptionHandler(responseGenerator); + protected final ResourceValidator resourceValidator = mock(ResourceValidator.class); + protected final ReferenceCleaner referenceCleaner = mock(ReferenceCleaner.class); + @SuppressWarnings("unchecked") + protected final AuthorizationRule authorizationRule = mock(AuthorizationRule.class); + protected final CurrentIdentityProvider currentIdentityProvider = mock(CurrentIdentityProvider.class); + protected final Response responseOkWithResourceIdVersion = mock(Response.class); + protected final Response responseForbiddenWithOperationOutcome = mock(Response.class); + protected final Response responseNotModifiedWithResourceIdVersion = mock(Response.class); + protected final Response responsePreconditionFailedWithResourceIdVersion = mock(Response.class); + + protected final S delegate; + protected final D dao; + + protected S resourceServiceSecure; + + public AbstractResourceServiceSecureTest(Class resourceClass, Class serviceClass, Class daoClass, + Supplier resouceSupplier, ResourceServiceSecureFactory resourceServiceSecureFactory) + { + this.resourceClass = resourceClass; + this.serviceClass = serviceClass; + this.daoClass = daoClass; + this.resouceSupplier = resouceSupplier; + this.resourceServiceSecureFactory = resourceServiceSecureFactory; + + delegate = mock(serviceClass); + dao = mock(daoClass); + } + + @Before + public void before() throws Exception + { + resourceServiceSecure = createResourceServiceSecure(); + resourceServiceSecure.setCurrentIdentityProvider(currentIdentityProvider); + + Method afterPropertiesSet = resourceServiceSecure.getClass().getMethod("afterPropertiesSet"); + afterPropertiesSet.invoke(resourceServiceSecure); + + Identity identity = mock(Identity.class); + when(identity.getName()).thenReturn("Test Identity"); + when(currentIdentityProvider.getCurrentIdentity()).thenReturn(identity); + + when(referenceCleaner.cleanLiteralReferences(any(resourceClass))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + when(responseOkWithResourceIdVersion.getStatusInfo()).thenReturn(Status.OK); + when(responseOkWithResourceIdVersion.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(responseOkWithResourceIdVersion.hasEntity()).thenReturn(true); + when(responseOkWithResourceIdVersion.getEntity()).thenReturn(createResourceWithIdAndVersion()); + + when(responseForbiddenWithOperationOutcome.getStatusInfo()).thenReturn(Status.FORBIDDEN); + when(responseForbiddenWithOperationOutcome.getStatus()).thenReturn(Status.FORBIDDEN.getStatusCode()); + when(responseForbiddenWithOperationOutcome.hasEntity()).thenReturn(true); + when(responseForbiddenWithOperationOutcome.getEntity()).thenReturn(new OperationOutcome()); + + when(responseNotModifiedWithResourceIdVersion.getStatusInfo()).thenReturn(Status.NOT_MODIFIED); + when(responseNotModifiedWithResourceIdVersion.getStatus()).thenReturn(Status.NOT_MODIFIED.getStatusCode()); + when(responseNotModifiedWithResourceIdVersion.hasEntity()).thenReturn(true); + when(responseNotModifiedWithResourceIdVersion.getEntity()).thenReturn(createResourceWithIdAndVersion()); + + when(responsePreconditionFailedWithResourceIdVersion.getStatusInfo()).thenReturn(Status.PRECONDITION_FAILED); + when(responsePreconditionFailedWithResourceIdVersion.getStatus()) + .thenReturn(Status.PRECONDITION_FAILED.getStatusCode()); + when(responsePreconditionFailedWithResourceIdVersion.hasEntity()).thenReturn(true); + when(responsePreconditionFailedWithResourceIdVersion.getEntity()).thenReturn(createResourceWithIdAndVersion()); + } + + protected final S createResourceServiceSecure() + { + return resourceServiceSecureFactory.create(delegate, SERVER_BASE, responseGenerator, + mock(ReferenceResolver.class), referenceCleaner, mock(ReferenceExtractor.class), dao, exceptionHandler, + mock(ParameterConverter.class), authorizationRule, resourceValidator, validationRules, + mock(DefaultProfileProvider.class)); + } + + protected final R createResource() + { + return resouceSupplier.get(); + } + + protected final R createResourceWithIdAndVersion() + { + R resource = createResource(); + resource.setIdElement( + new IdType(resourceClass.getAnnotation(ResourceDef.class).name(), UUID.randomUUID().toString(), "1")); + resource.getMeta().setVersionId("1"); + + return resource; + } + + @Test + public void readMustEnforceReadAuthorization() + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void vreadMustEnforceReadAuthorization() + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectForbiddenReadNotAllowed() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + } + + @Test + public void expectForbiddenVRreadNotAllowed() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + } + + @Test + public void expectOkReadAllowedWithStatusOk() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(resourceClass, response.getEntity().getClass()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectOkVReadAllowedWithStatusOk() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(resourceClass, response.getEntity().getClass()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectNoEntityAndStatusNotModifiedReadAllowedWithStatusNotModified() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseNotModifiedWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.NOT_MODIFIED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectNoEntityAndStatusNotModifiedVReadAllowedWithStatusNotModified() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseNotModifiedWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.NOT_MODIFIED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectNoEntityAndStatusPreconditionFailedReadAllowedWithStatusPreconditionFailed() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responsePreconditionFailedWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectNoEntityAndStatusPreconditionFailedVReadAllowedWithStatusPreconditionFailed() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())) + .thenReturn(responsePreconditionFailedWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectOperationOutcomeReadNoAuthorizationRuleCallOperationOutcome() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseForbiddenWithOperationOutcome); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectOperationOutcomeVReadNoAuthorizationRuleCallOperationOutcome() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseForbiddenWithOperationOutcome); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectForbiddenReadNoAuthorizationRuleCallUnexpectedResourceType() throws Exception + { + Response unexpectedResourceTpye = mock(Response.class); + when(unexpectedResourceTpye.getStatusInfo()).thenReturn(Status.OK); + when(unexpectedResourceTpye.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(unexpectedResourceTpye.getEntity()).thenReturn("Unexpected Resource Type"); + when(unexpectedResourceTpye.hasEntity()).thenReturn(true); + when(delegate.read(anyString(), any(), any())).thenReturn(unexpectedResourceTpye); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectForbiddenVReadNoAuthorizationRuleCallUnexpectedResourceType() throws Exception + { + Response unexpectedResourceTpye = mock(Response.class); + when(unexpectedResourceTpye.getStatusInfo()).thenReturn(Status.OK); + when(unexpectedResourceTpye.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(unexpectedResourceTpye.getEntity()).thenReturn("Unexpected Resource Type"); + when(unexpectedResourceTpye.hasEntity()).thenReturn(true); + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(unexpectedResourceTpye); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectStatusCodeReadNoAuthorizationRuleCallStatusCodeOnly() throws Exception + { + Response statusCodeOnly = mock(Response.class); + when(statusCodeOnly.getStatusInfo()).thenReturn(Status.PAYMENT_REQUIRED); + when(statusCodeOnly.getStatus()).thenReturn(Status.PAYMENT_REQUIRED.getStatusCode()); + + when(delegate.read(anyString(), any(), any())).thenReturn(statusCodeOnly); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PAYMENT_REQUIRED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectStatusCodeVReadNoAuthorizationRuleCallStatusCodeOnly() throws Exception + { + Response statusCodeOnly = mock(Response.class); + when(statusCodeOnly.getStatusInfo()).thenReturn(Status.PAYMENT_REQUIRED); + when(statusCodeOnly.getStatus()).thenReturn(Status.PAYMENT_REQUIRED.getStatusCode()); + + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(statusCodeOnly); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PAYMENT_REQUIRED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void createMustEnforceCreateAuthorization() + { + resourceServiceSecure.create(createResourceWithIdAndVersion(), mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectForbiddenCreateNotAllowed() throws Exception + { + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.create(createResourceWithIdAndVersion(), mock(UriInfo.class), + mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectCreatedCreateAllowed() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response responseCreated = mock(Response.class); + when(responseCreated.getStatusInfo()).thenReturn(Status.CREATED); + when(responseCreated.getStatus()).thenReturn(Status.CREATED.getStatusCode()); + when(responseCreated.getEntity()).thenReturn(resource); + when(responseCreated.hasEntity()).thenReturn(true); + when(delegate.create(any(), any(), any())).thenReturn(responseCreated); + + Response response = resourceServiceSecure.create(resource, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.CREATED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(resourceClass, response.getEntity().getClass()); + + verify(resourceValidator).validate(any()); + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectForbiddenCreateAllowedNonValidResouce() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + SingleValidationMessage validationMessage = new SingleValidationMessage(); + validationMessage.setSeverity(ResultSeverityEnum.ERROR); + + when(resourceValidator.validate(any())) + .thenReturn(new ValidationResult(FHIR_CONTEXT, List.of(validationMessage))); + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.create(resource, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(resourceValidator).validate(any()); + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectOkCreateAllowedOneExists() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = responseGenerator.oneExists(resource, "Test Criteria"); + when(delegate.create(any(), any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.create(resource, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(resourceValidator).validate(any()); + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectPreconditionFailedCreateAllowedMultipleExists() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response responsePreconditionFailed = responseGenerator + .multipleExists(resourceClass.getAnnotation(ResourceDef.class).name(), "Test Criteria"); + when(delegate.create(any(), any(), any())).thenReturn(responsePreconditionFailed); + + Response response = resourceServiceSecure.create(resource, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + + verify(resourceValidator).validate(any()); + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void historyBaseMustEnforceHistoryAuthorization() + { + when(delegate.history(any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void historyResouceMustEnforceHistoryAuthorization() + { + when(delegate.history(anyString(), any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.history("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectOkHistoryBaseAllowed() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(delegate.history(any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectOkHistoryResourceAllowed() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(delegate.history(anyString(), any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.history("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectBadRequesHistoryBaseAllowedInvalidRequest() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.BAD_REQUEST); + when(responseOk.getStatus()).thenReturn(Status.BAD_REQUEST.getStatusCode()); + when(delegate.history(any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectBadRequestHistoryResourceAllowedInvalidRequest() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseBadRequest = mock(Response.class); + when(responseBadRequest.getStatusInfo()).thenReturn(Status.BAD_REQUEST); + when(responseBadRequest.getStatus()).thenReturn(Status.BAD_REQUEST.getStatusCode()); + when(delegate.history(anyString(), any(), any())).thenReturn(responseBadRequest); + + Response response = resourceServiceSecure.history("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void updateConditionalFoundResourceNoIdMustEnforceUpdateAuthorization() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(1); + when(partialResult.getPartialResult()).thenReturn(List.of(createResourceWithIdAndVersion())); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + resourceServiceSecure.update(createResource(), uriInfo, mock(HttpHeaders.class)); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test + public void updateConditionalFoundResourceSameIdMustEnforceUpdateAuthorization() throws Exception + { + R existingResource = createResourceWithIdAndVersion(); + R updateResource = createResource(); + updateResource.setIdElement(existingResource.getIdElement().toVersionless()); + + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(1); + when(partialResult.getPartialResult()).thenReturn(List.of(existingResource)); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + resourceServiceSecure.update(updateResource, uriInfo, mock(HttpHeaders.class)); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test + public void updateConditionalNotFoundMustEnforceCreateAuthorization() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(0); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + resourceServiceSecure.update(createResource(), uriInfo, mock(HttpHeaders.class)); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verify(authorizationRule).reasonCreateAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectMethodNotAllowedConditionalUpdateNoAuthorizationRuleCallNoMathcingResourceInDbButResourceHasId() + throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(0); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.update(createResourceWithIdAndVersion(), uriInfo, + mock(HttpHeaders.class)); + assertEquals(Status.METHOD_NOT_ALLOWED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectBadRequestConditionalUpdateNoAuthorizationRuleCallFoundResourceDifferentId() throws Exception + { + R existingResource = createResourceWithIdAndVersion(); + R updateResource = createResourceWithIdAndVersion(); + + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(1); + when(partialResult.getPartialResult()).thenReturn(List.of(existingResource)); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.update(updateResource, uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectBadRequestConditionalUpdateNoAuthorizationRuleCallUnsupportedQueryParameter() throws Exception + { + R updateResource = createResourceWithIdAndVersion(); + + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(searchQuery.getUnsupportedQueryParameters()) + .thenReturn(List.of(new SearchQueryParameterError(SearchQueryParameterErrorType.UNSUPPORTED_PARAMETER, + "ParameterTestName", "Parameter Test Value"))); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + try + { + resourceServiceSecure.update(updateResource, uriInfo, mock(HttpHeaders.class)); + fail("WebApplicationException expected"); + } + catch (WebApplicationException e) + { + assertEquals(Status.BAD_REQUEST, e.getResponse().getStatusInfo()); + assertTrue(e.getResponse().hasEntity()); + assertEquals(OperationOutcome.class, e.getResponse().getEntity().getClass()); + } + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verifyNoMoreInteractions(dao); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectPreconditionFailedConditionalUpdateNoAuthorizationRuleCallFoundResourceDifferentId() + throws Exception + { + R updateResource = createResourceWithIdAndVersion(); + + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(2); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.update(updateResource, uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoInteractions(authorizationRule); + } + + @Test + public void updateMustEnforceUpdateAuthorization() throws Exception + { + when(dao.read(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(delegate.update(anyString(), any(resourceClass), any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.update("some-id", createResource(), mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(dao).read(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test + public void expectMethodNotAllowedUpdateNoAuthorizationRuleCallResourceNotInDb() throws Exception + { + when(dao.read(any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.update("some-id", createResource(), mock(UriInfo.class), + mock(HttpHeaders.class)); + assertEquals(Status.METHOD_NOT_ALLOWED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).read(any()); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectOkUpdateAllowed() throws Exception + { + when(dao.read(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonUpdateAllowed(any(), any(resourceClass), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + + when(delegate.update(anyString(), any(resourceClass), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + Response response = resourceServiceSecure.update("some-id", createResource(), mock(UriInfo.class), + mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(dao).read(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test(expected = IllegalStateException.class) + public void expectIllegalStateExceptionUpdateSameResources() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + when(dao.read(any())).thenReturn(Optional.of(resource)); + + try + { + resourceServiceSecure.update("some-id", resource, mock(UriInfo.class), mock(HttpHeaders.class)); + } + finally + { + verify(dao).read(any()); + verifyNoInteractions(authorizationRule); + } + } + + @Test + public void expectForbiddenUpdateAllowedDelegateDuplicateResource() throws Exception + { + when(dao.read(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonUpdateAllowed(any(), any(resourceClass), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + + when(delegate.update(anyString(), any(resourceClass), any(), any())) + .thenThrow(new WebApplicationException(Response.status(Status.FORBIDDEN).build())); + + try + { + resourceServiceSecure.update("some-id", createResource(), mock(UriInfo.class), mock(HttpHeaders.class)); + fail("WebApplicationException expected"); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN, e.getResponse().getStatusInfo()); + } + + verify(dao).read(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test + public void deleteMustEnforceDeleteAuthorization() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(delegate.delete(anyString(), any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.delete("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectNotFoundDeleteNoAuthorizationRuleCallResourceNotInDb() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.delete("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.NOT_FOUND, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectOkDeleteAllowed() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonDeleteAllowed(any(Identity.class), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + + when(delegate.delete(anyString(), any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.delete("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectNotFoundDeleteAllowedNotFoundWhileDeleting() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonDeleteAllowed(any(Identity.class), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + + when(delegate.delete(anyString(), any(), any())) + .thenThrow(new WebApplicationException(Response.status(Status.NOT_FOUND).build())); + + try + { + resourceServiceSecure.delete("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + fail("WebApplicationException expected"); + } + catch (WebApplicationException e) + { + assertEquals(Status.NOT_FOUND, e.getResponse().getStatusInfo()); + } + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void deleteConditionalMustEnforceDeleteAuthorization() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(1); + when(partialResult.getPartialResult()).thenReturn(List.of(createResourceWithIdAndVersion())); + when(dao.search(any())).thenReturn(partialResult); + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + + when(delegate.delete(anyString(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + resourceServiceSecure.delete(uriInfo, mock(HttpHeaders.class)); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verify(authorizationRule).reasonDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectBadRequestConditionalDeleteNoAuthorizationRuleCallUnsupportedQueryParameter() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(searchQuery.getUnsupportedQueryParameters()) + .thenReturn(List.of(new SearchQueryParameterError(SearchQueryParameterErrorType.UNSUPPORTED_PARAMETER, + "ParameterTestName", "Parameter Test Value"))); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.delete(uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verifyNoMoreInteractions(dao); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectPreconditionFailedDeleteAllowedMultipleExists() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(2); + when(dao.search(any())).thenReturn(partialResult); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.delete(uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoMoreInteractions(dao); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectNoContentDeleteAlloweNoneExists() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(0); + when(dao.search(any())).thenReturn(partialResult); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.delete(uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.NO_CONTENT, response.getStatusInfo()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoMoreInteractions(dao); + verifyNoInteractions(authorizationRule); + } + + @Test + public void searchMustEnforceSearchAuthorization() throws Exception + { + when(delegate.search(any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.search(mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonSearchAllowed(any(Identity.class)); + } + + @Test + public void expectOkSearchAllowed() throws Exception + { + when(authorizationRule.reasonSearchAllowed(any(Identity.class))).thenReturn(Optional.of("Test Reason")); + when(delegate.search(any(), any())).thenReturn(Response.ok().build()); + + Response response = resourceServiceSecure.search(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(authorizationRule).reasonSearchAllowed(any(Identity.class)); + } + + @Test + public void expectBadRequestSearchAllowedUnsupportedParameter() throws Exception + { + when(authorizationRule.reasonSearchAllowed(any(Identity.class))).thenReturn(Optional.of("Test Reason")); + when(delegate.search(any(), any())).thenReturn(Response.status(Status.BAD_REQUEST).build()); + + Response response = resourceServiceSecure.search(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + + verify(authorizationRule).reasonSearchAllowed(any(Identity.class)); + } + + @Test + public void deletePermanentlyMustEnforcePermanentDeleteAuthorization() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(delegate.deletePermanently(anyString(), anyString(), any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.deletePermanently(PERMANENT_DELETE_PATH, "some-id", mock(UriInfo.class), + mock(HttpHeaders.class)); + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectNotFoundDeletePermanentlyNoAuthorizationRuleCallResourceNotInDb() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.deletePermanently(PERMANENT_DELETE_PATH, "some-id", + mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.NOT_FOUND, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectOkDeletePermanentlyAllowed() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + + when(delegate.deletePermanently(anyString(), anyString(), any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.deletePermanently(PERMANENT_DELETE_PATH, "some-id", + mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + + @Test + public void expectNotFoundDeletePermanentlyAllowedNotFoundWhileDeletingPermanently() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + + when(delegate.deletePermanently(anyString(), anyString(), any(), any())) + .thenThrow(new WebApplicationException(Response.status(Status.NOT_FOUND).build())); + + try + { + resourceServiceSecure.deletePermanently(PERMANENT_DELETE_PATH, "some-id", mock(UriInfo.class), + mock(HttpHeaders.class)); + fail("WebApplicationException expected"); + } + catch (WebApplicationException e) + { + assertEquals(Status.NOT_FOUND, e.getResponse().getStatusInfo()); + } + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass)); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecureTest.java new file mode 100644 index 000000000..42ea0dcf4 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecureTest.java @@ -0,0 +1,73 @@ +/* + * 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.webservice.secure; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.InputStream; + +import org.hl7.fhir.r4.model.Binary; +import org.junit.Test; + +import dev.dsf.fhir.dao.BinaryDao; +import dev.dsf.fhir.webservice.specification.BinaryService; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.UriInfo; + +public class BinaryServiceSecureTest extends AbstractResourceServiceSecureTest +{ + public BinaryServiceSecureTest() + { + super(Binary.class, BinaryService.class, BinaryDao.class, Binary::new, BinaryServiceSecure::new); + } + + @Test + public void readHeadMustEnforceReadAuthorization() + { + when(delegate.readHead(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.readHead("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void vreadHeadMustEnforceReadAuthorization() + { + when(delegate.vreadHead(anyString(), anyLong(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.vreadHead("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test(expected = UnsupportedOperationException.class) + public void expectUnsupportedOperationExceptionCreateInputStream() throws Exception + { + resourceServiceSecure.create(mock(InputStream.class), mock(UriInfo.class), mock(HttpHeaders.class)); + } + + @Test(expected = UnsupportedOperationException.class) + public void expectUnsupportedOperationExceptionUpdateInputStream() throws Exception + { + resourceServiceSecure.update("some-id", mock(InputStream.class), mock(UriInfo.class), mock(HttpHeaders.class)); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/ResourceServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/ResourceServiceSecureTest.java new file mode 100644 index 000000000..8235ddc7c --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/ResourceServiceSecureTest.java @@ -0,0 +1,206 @@ +/* + * 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.webservice.secure; + +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.DocumentReference; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.HealthcareService; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.ResearchStudy; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.Subscription; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import dev.dsf.fhir.dao.ActivityDefinitionDao; +import dev.dsf.fhir.dao.BundleDao; +import dev.dsf.fhir.dao.CodeSystemDao; +import dev.dsf.fhir.dao.DocumentReferenceDao; +import dev.dsf.fhir.dao.EndpointDao; +import dev.dsf.fhir.dao.GroupDao; +import dev.dsf.fhir.dao.HealthcareServiceDao; +import dev.dsf.fhir.dao.LibraryDao; +import dev.dsf.fhir.dao.LocationDao; +import dev.dsf.fhir.dao.MeasureDao; +import dev.dsf.fhir.dao.MeasureReportDao; +import dev.dsf.fhir.dao.NamingSystemDao; +import dev.dsf.fhir.dao.OrganizationAffiliationDao; +import dev.dsf.fhir.dao.OrganizationDao; +import dev.dsf.fhir.dao.PatientDao; +import dev.dsf.fhir.dao.PractitionerDao; +import dev.dsf.fhir.dao.PractitionerRoleDao; +import dev.dsf.fhir.dao.ProvenanceDao; +import dev.dsf.fhir.dao.QuestionnaireDao; +import dev.dsf.fhir.dao.QuestionnaireResponseDao; +import dev.dsf.fhir.dao.ResearchStudyDao; +import dev.dsf.fhir.dao.ResourceDao; +import dev.dsf.fhir.dao.SubscriptionDao; +import dev.dsf.fhir.dao.ValueSetDao; +import dev.dsf.fhir.webservice.specification.ActivityDefinitionService; +import dev.dsf.fhir.webservice.specification.BasicResourceService; +import dev.dsf.fhir.webservice.specification.BundleService; +import dev.dsf.fhir.webservice.specification.CodeSystemService; +import dev.dsf.fhir.webservice.specification.DocumentReferenceService; +import dev.dsf.fhir.webservice.specification.EndpointService; +import dev.dsf.fhir.webservice.specification.GroupService; +import dev.dsf.fhir.webservice.specification.HealthcareServiceService; +import dev.dsf.fhir.webservice.specification.LibraryService; +import dev.dsf.fhir.webservice.specification.LocationService; +import dev.dsf.fhir.webservice.specification.MeasureReportService; +import dev.dsf.fhir.webservice.specification.MeasureService; +import dev.dsf.fhir.webservice.specification.NamingSystemService; +import dev.dsf.fhir.webservice.specification.OrganizationAffiliationService; +import dev.dsf.fhir.webservice.specification.OrganizationService; +import dev.dsf.fhir.webservice.specification.PatientService; +import dev.dsf.fhir.webservice.specification.PractitionerRoleService; +import dev.dsf.fhir.webservice.specification.PractitionerService; +import dev.dsf.fhir.webservice.specification.ProvenanceService; +import dev.dsf.fhir.webservice.specification.QuestionnaireResponseService; +import dev.dsf.fhir.webservice.specification.QuestionnaireService; +import dev.dsf.fhir.webservice.specification.ResearchStudyService; +import dev.dsf.fhir.webservice.specification.SubscriptionService; +import dev.dsf.fhir.webservice.specification.ValueSetService; + +@RunWith(Parameterized.class) +public class ResourceServiceSecureTest + extends AbstractResourceServiceSecureTest, ResourceDao> +{ + @Parameters(name = "{0}") + public static Collection data() + { + return List.of(new Object[][] { + + { "ActivityDefinition", ActivityDefinition.class, ActivityDefinitionService.class, + ActivityDefinitionDao.class, (Supplier) ActivityDefinition::new, + (ResourceServiceSecureFactory) ActivityDefinitionServiceSecure::new }, + + { "Bundle", Bundle.class, BundleService.class, BundleDao.class, (Supplier) Bundle::new, + (ResourceServiceSecureFactory) BundleServiceSecure::new }, + + { "CodeSystem", CodeSystem.class, CodeSystemService.class, CodeSystemDao.class, + (Supplier) CodeSystem::new, + (ResourceServiceSecureFactory) CodeSystemServiceSecure::new }, + + { "DocumentReference", DocumentReference.class, DocumentReferenceService.class, + DocumentReferenceDao.class, (Supplier) DocumentReference::new, + (ResourceServiceSecureFactory) DocumentReferenceServiceSecure::new }, + + { "Endpoint", Endpoint.class, EndpointService.class, EndpointDao.class, + (Supplier) Endpoint::new, + (ResourceServiceSecureFactory) EndpointServiceSecure::new }, + + { "Group", Group.class, GroupService.class, GroupDao.class, (Supplier) Group::new, + (ResourceServiceSecureFactory) GroupServiceSecure::new }, + + { "HealthcareService", HealthcareService.class, HealthcareServiceService.class, + HealthcareServiceDao.class, (Supplier) HealthcareService::new, + (ResourceServiceSecureFactory) HealthcareServiceServiceSecure::new }, + + { "Library", Library.class, LibraryService.class, LibraryDao.class, (Supplier) Library::new, + (ResourceServiceSecureFactory) LibraryServiceSecure::new }, + + { "Location", Location.class, LocationService.class, LocationDao.class, + (Supplier) Location::new, + (ResourceServiceSecureFactory) LocationServiceSecure::new }, + + { "Measure", Measure.class, MeasureService.class, MeasureDao.class, (Supplier) Measure::new, + (ResourceServiceSecureFactory) MeasureServiceSecure::new }, + + { "MeasureReport", MeasureReport.class, MeasureReportService.class, MeasureReportDao.class, + (Supplier) MeasureReport::new, + (ResourceServiceSecureFactory) MeasureReportServiceSecure::new }, + + { "NamingSystem", NamingSystem.class, NamingSystemService.class, NamingSystemDao.class, + (Supplier) NamingSystem::new, + (ResourceServiceSecureFactory) NamingSystemServiceSecure::new }, + + { "OrganizationAffiliation", OrganizationAffiliation.class, OrganizationAffiliationService.class, + OrganizationAffiliationDao.class, + (Supplier) OrganizationAffiliation::new, + (ResourceServiceSecureFactory) OrganizationAffiliationServiceSecure::new }, + + { "Organization", Organization.class, OrganizationService.class, OrganizationDao.class, + (Supplier) Organization::new, + (ResourceServiceSecureFactory) OrganizationServiceSecure::new }, + + { "Patient", Patient.class, PatientService.class, PatientDao.class, (Supplier) Patient::new, + (ResourceServiceSecureFactory) PatientServiceSecure::new }, + + { "Practitioner", Practitioner.class, PractitionerService.class, PractitionerDao.class, + (Supplier) Practitioner::new, + (ResourceServiceSecureFactory) PractitionerServiceSecure::new }, + + { "PractitionerRole", PractitionerRole.class, PractitionerRoleService.class, PractitionerRoleDao.class, + (Supplier) PractitionerRole::new, + (ResourceServiceSecureFactory) PractitionerRoleServiceSecure::new }, + + { "Provenance", Provenance.class, ProvenanceService.class, ProvenanceDao.class, + (Supplier) Provenance::new, + (ResourceServiceSecureFactory) ProvenanceServiceSecure::new }, + + { "Questionnaire", Questionnaire.class, QuestionnaireService.class, QuestionnaireDao.class, + (Supplier) Questionnaire::new, + (ResourceServiceSecureFactory) QuestionnaireServiceSecure::new }, + + { "QuestionnaireResponse", QuestionnaireResponse.class, QuestionnaireResponseService.class, + QuestionnaireResponseDao.class, (Supplier) QuestionnaireResponse::new, + (ResourceServiceSecureFactory) QuestionnaireResponseServiceSecure::new }, + + { "ResearchStudy", ResearchStudy.class, ResearchStudyService.class, ResearchStudyDao.class, + (Supplier) ResearchStudy::new, + (ResourceServiceSecureFactory) ResearchStudyServiceSecure::new }, + + { "Subscription", Subscription.class, SubscriptionService.class, SubscriptionDao.class, + (Supplier) Subscription::new, + (ResourceServiceSecureFactory) SubscriptionServiceSecure::new }, + + { "ValueSet", ValueSet.class, ValueSetService.class, ValueSetDao.class, + (Supplier) ValueSet::new, + (ResourceServiceSecureFactory) ValueSetServiceSecure::new }, + + }); + } + + public ResourceServiceSecureTest(String label, Class resourceClass, + Class> serviceClass, Class> daoClass, + Supplier resouceSupplier, + ResourceServiceSecureFactory, ResourceDao> resourceServiceSecureFactory) + { + super(resourceClass, serviceClass, daoClass, resouceSupplier, resourceServiceSecureFactory); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/RootServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/RootServiceSecureTest.java new file mode 100644 index 000000000..907607f65 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/RootServiceSecureTest.java @@ -0,0 +1,153 @@ +/* + * 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.webservice.secure; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.EnumSet; +import java.util.Optional; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; +import org.junit.Before; +import org.junit.Test; + +import dev.dsf.common.auth.conf.Identity; +import dev.dsf.fhir.authentication.CurrentIdentityProvider; +import dev.dsf.fhir.authorization.AuthorizationRule; +import dev.dsf.fhir.help.ResponseGenerator; +import dev.dsf.fhir.service.ReferenceResolver; +import dev.dsf.fhir.webservice.specification.RootService; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; + +public class RootServiceSecureTest +{ + private static final String SERVER_BASE = "https://dsf.test/fhir"; + + private final RootService delegate = mock(RootService.class); + + @SuppressWarnings("unchecked") + private final AuthorizationRule authorizationRule = mock(AuthorizationRule.class); + private final CurrentIdentityProvider currentIdentityProvider = mock(CurrentIdentityProvider.class); + + private RootServiceSecure rootServiceSecure; + + @Before + public void before() throws Exception + { + rootServiceSecure = new RootServiceSecure(delegate, SERVER_BASE, new ResponseGenerator(SERVER_BASE), + mock(ReferenceResolver.class), authorizationRule); + rootServiceSecure.afterPropertiesSet(); + + rootServiceSecure.setCurrentIdentityProvider(currentIdentityProvider); + + Identity identity = mock(Identity.class); + when(identity.getName()).thenReturn("Test Identity"); + when(currentIdentityProvider.getCurrentIdentity()).thenReturn(identity); + } + + @Test + public void getAllowedForAll() throws Exception + { + when(delegate.root(any(), any())).thenReturn(mock(Response.class)); + + rootServiceSecure.root(mock(UriInfo.class), mock(HttpHeaders.class)); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void handleBundleAllowedBatch() + { + when(delegate.handleBundle(any(), any(), any())).thenReturn(mock(Response.class)); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + rootServiceSecure.handleBundle(b, mock(UriInfo.class), mock(HttpHeaders.class)); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void handleBundleAllowedTransaction() + { + when(delegate.handleBundle(any(), any(), any())).thenReturn(mock(Response.class)); + + Bundle b = new Bundle(); + b.setType(BundleType.TRANSACTION); + + rootServiceSecure.handleBundle(b, mock(UriInfo.class), mock(HttpHeaders.class)); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectForbiddenHandleBundleNotBatchNotTransaction() + { + EnumSet forbiddenTypes = EnumSet.complementOf(EnumSet.of(BundleType.BATCH, BundleType.TRANSACTION)); + for (BundleType type : forbiddenTypes) + { + Bundle b = new Bundle(); + b.setType(type); + + Response response = rootServiceSecure.handleBundle(b, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + } + + verifyNoInteractions(authorizationRule); + } + + @Test + public void historyMustEnforceHistoryAuthorization() + { + when(delegate.history(any(), any())).thenReturn(mock(Response.class)); + + rootServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectOkHistoryAllowed() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(delegate.history(any(), any())).thenReturn(responseOk); + + Response response = rootServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecureTest.java new file mode 100644 index 000000000..a4dff0d10 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecureTest.java @@ -0,0 +1,110 @@ +/* + * 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.webservice.secure; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.UrlType; +import org.junit.Test; + +import dev.dsf.fhir.dao.StructureDefinitionDao; +import dev.dsf.fhir.webservice.specification.StructureDefinitionService; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.UriInfo; + +public class StructureDefinitionServiceSecureTest extends + AbstractResourceServiceSecureTest +{ + private static final String SNAPSHOT_PATH = "$snapshot"; + + public StructureDefinitionServiceSecureTest() + { + super(StructureDefinition.class, StructureDefinitionService.class, StructureDefinitionDao.class, + StructureDefinition::new, StructureDefinitionServiceSecure::new); + } + + @Test + public void getSnapshotExistingMustEnforceReadAuthorization() + { + when(delegate.getSnapshotExisting(anyString(), anyString(), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.getSnapshotExisting(SNAPSHOT_PATH, "some-id", mock(UriInfo.class), + mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void postSnapshotExistingMustEnforceReadAuthorization() + { + when(delegate.postSnapshotExisting(anyString(), anyString(), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.postSnapshotExisting(SNAPSHOT_PATH, "some-id", mock(UriInfo.class), + mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void getSnapshotNewMustEnforceReadAuthorization() + { + when(delegate.getSnapshotNew(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.getSnapshotNew(SNAPSHOT_PATH, mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void postSnapshotNewMustEnforceReadAuthorizationIfInvokedWithUrlParameter() + { + Parameters parameters = mock(Parameters.class); + when(parameters.getParameter("url")).thenReturn(new ParametersParameterComponent().setValue(new UrlType())); + + when(delegate.postSnapshotNew(anyString(), eq(parameters), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.postSnapshotNew(SNAPSHOT_PATH, parameters, mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void postSnapshotNewMustNotEnforceReadAuthorizationIfInvokedWithResourceParameter() + { + Parameters parameters = mock(Parameters.class); + when(parameters.getParameter("resource")) + .thenReturn(new ParametersParameterComponent().setResource(new StructureDefinition())); + + when(delegate.postSnapshotNew(anyString(), eq(parameters), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.postSnapshotNew(SNAPSHOT_PATH, parameters, mock(UriInfo.class), mock(HttpHeaders.class)); + + verifyNoInteractions(authorizationRule); + } +} diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java index eb764ccd3..de0d8c139 100644 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java @@ -154,6 +154,12 @@ public CapabilityStatement getConformance() return retry(() -> delegate.getConformance()); } + @Override + public StructureDefinition getSnapshot(String id) + { + return retry(() -> delegate.getSnapshot(id)); + } + @Override public StructureDefinition generateSnapshot(StructureDefinition differential) { diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java index b288c5eca..ed687e375 100644 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java @@ -177,6 +177,8 @@ BinaryInputStream readBinary(String id, String version, MediaType mediaType, Lon CapabilityStatement getConformance(); + StructureDefinition getSnapshot(String id); + StructureDefinition generateSnapshot(String url); StructureDefinition generateSnapshot(StructureDefinition differential); diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java index ff6e90bfa..897eae502 100755 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java @@ -833,6 +833,22 @@ public CapabilityStatement getConformance() throw handleError(response); } + @Override + public StructureDefinition getSnapshot(String id) + { + Objects.requireNonNull(id, "id"); + + Response response = getResource().path(StructureDefinition.class.getAnnotation(ResourceDef.class).name()) + .path(id).path("$snapshot").request().accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(StructureDefinition.class); + else + throw handleError(response); + } + @Override public StructureDefinition generateSnapshot(String url) {