From c3c9fe2bbaaa3488f7ca5e592665d4eb34cc09aa Mon Sep 17 00:00:00 2001 From: cjmamo <823038+cjmamo@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:02:07 +0100 Subject: [PATCH 1/2] feat(DHIS2-18837): backport #19763 --- .../common/auth/ApiHeadersAuthScheme.java | 88 +++++++ .../common/auth/ApiQueryParamsAuthScheme.java | 88 +++++++ ...TokenAuth.java => ApiTokenAuthScheme.java} | 29 ++- .../auth/{Auth.java => AuthScheme.java} | 18 +- .../dhis/common/auth/HttpBasicAuthScheme.java | 92 +++++++ .../dhis/eventhook/targets/WebhookTarget.java | 4 +- .../AddIconRequest.java} | 48 ++-- .../main/java/org/hisp/dhis/route/Route.java | 6 +- .../targets/auth/AbstractAuthSchemeTest.java | 60 +++++ .../auth/ApiHeadersAuthSchemeTest.java | 57 +++++ .../auth/ApiQueryParamsAuthSchemeTest.java | 55 +++++ ...hTest.java => ApiTokenAuthSchemeTest.java} | 28 ++- ...Test.java => HttpBasicAuthSchemeTest.java} | 29 ++- .../org/hisp/dhis/route/RouteService.java | 53 ++-- .../hooks/RouteObjectBundleHook.java | 28 +-- .../eventhook/EventHookSecretManager.java | 54 ++--- .../eventhook/handlers/WebhookHandler.java | 19 +- .../FieldFilterSimpleBeanPropertyFilter.java | 12 +- .../org/hisp/dhis/usertype/UserTypes.hbm.xml | 2 +- .../controller/EventHookControllerTest.java | 10 +- .../controller/RouteControllerTest.java | 226 ++++++++++++++++++ 21 files changed, 852 insertions(+), 154 deletions(-) create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiHeadersAuthScheme.java create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiQueryParamsAuthScheme.java rename dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/{ApiTokenAuth.java => ApiTokenAuthScheme.java} (75%) rename dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/{Auth.java => AuthScheme.java} (75%) create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/HttpBasicAuthScheme.java rename dhis-2/dhis-api/src/main/java/org/hisp/dhis/{common/auth/HttpBasicAuth.java => icon/AddIconRequest.java} (66%) create mode 100644 dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/AbstractAuthSchemeTest.java create mode 100644 dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiHeadersAuthSchemeTest.java create mode 100644 dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiQueryParamsAuthSchemeTest.java rename dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/{ApiTokenAuthTest.java => ApiTokenAuthSchemeTest.java} (71%) rename dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/{HttpBasicAuthTest.java => HttpBasicAuthSchemeTest.java} (70%) create mode 100644 dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/RouteControllerTest.java diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiHeadersAuthScheme.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiHeadersAuthScheme.java new file mode 100644 index 000000000000..0a783956d065 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiHeadersAuthScheme.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.util.MultiValueMap; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class ApiHeadersAuthScheme extends AuthScheme { + public static final String API_HEADERS_TYPE = "api-headers"; + + @JsonProperty(required = true) + private Map headers = new HashMap<>(); + + public ApiHeadersAuthScheme() { + super(API_HEADERS_TYPE); + } + + @Override + public void apply( + MultiValueMap headers, MultiValueMap queryParams) { + for (Map.Entry header : this.headers.entrySet()) { + headers.set(header.getKey(), header.getValue()); + } + } + + @Override + public ApiHeadersAuthScheme encrypt(UnaryOperator encryptFunc) { + Map encryptedHeaders = + headers.entrySet().stream() + .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), encryptFunc.apply(e.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return copy(encryptedHeaders); + } + + @Override + public ApiHeadersAuthScheme decrypt(UnaryOperator decryptFunc) { + Map encryptedHeaders = + headers.entrySet().stream() + .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), decryptFunc.apply(e.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return copy(encryptedHeaders); + } + + protected ApiHeadersAuthScheme copy(Map headers) { + ApiHeadersAuthScheme apiHeadersAuth = new ApiHeadersAuthScheme(); + apiHeadersAuth.setHeaders(headers); + + return apiHeadersAuth; + } +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiQueryParamsAuthScheme.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiQueryParamsAuthScheme.java new file mode 100644 index 000000000000..108799cc7e7f --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiQueryParamsAuthScheme.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.util.MultiValueMap; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class ApiQueryParamsAuthScheme extends AuthScheme { + public static final String API_QUERY_PARAMS_TYPE = "api-query-params"; + + @JsonProperty(required = true) + private Map queryParams = new HashMap<>(); + + public ApiQueryParamsAuthScheme() { + super(API_QUERY_PARAMS_TYPE); + } + + @Override + public void apply( + MultiValueMap headers, MultiValueMap queryParams) { + for (Map.Entry queryParam : this.queryParams.entrySet()) { + queryParams.set(queryParam.getKey(), queryParam.getValue()); + } + } + + @Override + public ApiQueryParamsAuthScheme encrypt(UnaryOperator encryptFunc) { + Map encryptedQueryParams = + queryParams.entrySet().stream() + .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), encryptFunc.apply(e.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return copy(encryptedQueryParams); + } + + @Override + public ApiQueryParamsAuthScheme decrypt(UnaryOperator decryptFunc) { + Map encryptedQueryParams = + queryParams.entrySet().stream() + .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), decryptFunc.apply(e.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return copy(encryptedQueryParams); + } + + protected ApiQueryParamsAuthScheme copy(Map queryParams) { + ApiQueryParamsAuthScheme apiQueryParamsAuth = new ApiQueryParamsAuthScheme(); + apiQueryParamsAuth.setQueryParams(queryParams); + + return apiQueryParamsAuth; + } +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiTokenAuth.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiTokenAuthScheme.java similarity index 75% rename from dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiTokenAuth.java rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiTokenAuthScheme.java index cfe8cf6d9b30..e8d0071fc26d 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiTokenAuth.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/ApiTokenAuthScheme.java @@ -28,6 +28,7 @@ package org.hisp.dhis.common.auth; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.function.UnaryOperator; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -45,22 +46,40 @@ @Setter @EqualsAndHashCode(callSuper = true) @Accessors(chain = true) -public class ApiTokenAuth extends Auth { - public static final String TYPE = "api-token"; +public class ApiTokenAuthScheme extends AuthScheme { + public static final String API_TOKEN_TYPE = "api-token"; @JsonProperty(required = true) private String token; - public ApiTokenAuth() { - super(TYPE); + public ApiTokenAuthScheme() { + super(API_TOKEN_TYPE); } @Override - public void apply(MultiValueMap headers) { + public void apply( + MultiValueMap headers, MultiValueMap queryParams) { if (!StringUtils.hasText(token)) { return; } headers.set("Authorization", "ApiToken " + token); } + + @Override + public ApiTokenAuthScheme encrypt(UnaryOperator encryptFunc) { + return copy(encryptFunc.apply(token)); + } + + @Override + public AuthScheme decrypt(UnaryOperator decryptFunc) { + return copy(decryptFunc.apply(token)); + } + + protected ApiTokenAuthScheme copy(String token) { + ApiTokenAuthScheme newApiTokenAuth = new ApiTokenAuthScheme(); + newApiTokenAuth.setToken(token); + + return newApiTokenAuth; + } } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/Auth.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/AuthScheme.java similarity index 75% rename from dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/Auth.java rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/AuthScheme.java index 2f262cd8942d..c05f1fe712be 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/Auth.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/AuthScheme.java @@ -32,6 +32,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import java.io.Serializable; +import java.util.function.UnaryOperator; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -50,16 +51,23 @@ include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = HttpBasicAuth.class, name = "http-basic"), - @JsonSubTypes.Type(value = ApiTokenAuth.class, name = "api-token") + @JsonSubTypes.Type(value = HttpBasicAuthScheme.class, name = "http-basic"), + @JsonSubTypes.Type(value = ApiTokenAuthScheme.class, name = "api-token"), + @JsonSubTypes.Type(value = ApiHeadersAuthScheme.class, name = "api-headers"), + @JsonSubTypes.Type(value = ApiQueryParamsAuthScheme.class, name = "api-query-params") }) -public abstract class Auth implements Serializable { +public abstract class AuthScheme implements Serializable { @JsonProperty protected final String type; @JsonCreator - protected Auth(@JsonProperty("type") String type) { + protected AuthScheme(@JsonProperty("type") String type) { this.type = type; } - public abstract void apply(MultiValueMap headers); + public abstract void apply( + MultiValueMap headers, MultiValueMap queryParams); + + public abstract AuthScheme encrypt(UnaryOperator encryptFunc); + + public abstract AuthScheme decrypt(UnaryOperator decryptFunc); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/HttpBasicAuthScheme.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/HttpBasicAuthScheme.java new file mode 100644 index 000000000000..2f2acc092247 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/HttpBasicAuthScheme.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Base64; +import java.util.function.UnaryOperator; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * @author Morten Olav Hansen + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class HttpBasicAuthScheme extends AuthScheme { + public static final String HTTP_BASIC_TYPE = "http-basic"; + + @JsonProperty(required = true) + private String username; + + @JsonProperty(required = true) + private String password; + + public HttpBasicAuthScheme() { + super(HTTP_BASIC_TYPE); + } + + @Override + public void apply( + MultiValueMap headers, MultiValueMap queryParams) { + if (!(StringUtils.hasText(username) && StringUtils.hasText(password))) { + return; + } + + headers.add("Authorization", getBasicAuth(username, password)); + } + + @Override + public HttpBasicAuthScheme encrypt(UnaryOperator encryptFunc) { + return copy(encryptFunc.apply(password)); + } + + @Override + public HttpBasicAuthScheme decrypt(UnaryOperator decryptFunc) { + return copy(decryptFunc.apply(password)); + } + + protected HttpBasicAuthScheme copy(String password) { + HttpBasicAuthScheme newHttpBasicAuth = new HttpBasicAuthScheme(); + newHttpBasicAuth.setUsername(username); + newHttpBasicAuth.setPassword(password); + + return newHttpBasicAuth; + } + + private String getBasicAuth(String username, String password) { + String string = String.format("%s:%s", username, password); + return "Basic " + Base64.getEncoder().encodeToString(string.getBytes()); + } +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/eventhook/targets/WebhookTarget.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/eventhook/targets/WebhookTarget.java index 3bd0adaddaf6..8e944d881bcf 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/eventhook/targets/WebhookTarget.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/eventhook/targets/WebhookTarget.java @@ -35,7 +35,7 @@ import lombok.Setter; import lombok.experimental.Accessors; import org.hisp.dhis.common.CodeGenerator; -import org.hisp.dhis.common.auth.Auth; +import org.hisp.dhis.common.auth.AuthScheme; import org.hisp.dhis.eventhook.Target; /** @@ -59,7 +59,7 @@ public class WebhookTarget extends Target { @JsonProperty private Map headers = new HashMap<>(); - @JsonProperty private Auth auth; + @JsonProperty private AuthScheme auth; public WebhookTarget() { super(TYPE); diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/HttpBasicAuth.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/icon/AddIconRequest.java similarity index 66% rename from dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/HttpBasicAuth.java rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/icon/AddIconRequest.java index 0c0545033ac7..cda41d51e3ed 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/HttpBasicAuth.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/icon/AddIconRequest.java @@ -25,45 +25,29 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.common.auth; +package org.hisp.dhis.icon; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Base64; -import lombok.EqualsAndHashCode; +import java.util.HashSet; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; +import lombok.NoArgsConstructor; -/** - * @author Morten Olav Hansen - */ +/** User input when creating a new {@link Icon} */ +@Builder(toBuilder = true) @Getter -@Setter -@EqualsAndHashCode(callSuper = true) -@Accessors(chain = true) -public class HttpBasicAuth extends Auth { - public static final String TYPE = "http-basic"; - - @JsonProperty(required = true) - private String username; +@AllArgsConstructor +@NoArgsConstructor +public class AddIconRequest { @JsonProperty(required = true) - private String password; + private String key; - public HttpBasicAuth() { - super(TYPE); - } + @JsonProperty private String description; + @JsonProperty private Set keywords = new HashSet<>(); - @Override - public void apply(MultiValueMap headers) { - if (!(StringUtils.hasText(username) && StringUtils.hasText(password))) { - return; - } - - headers.add( - "Authorization", - "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes())); - } + @JsonProperty(required = true) + private String fileResourceId; } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/route/Route.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/route/Route.java index fdfa787158f0..82e8f32c44a9 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/route/Route.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/route/Route.java @@ -38,7 +38,7 @@ import lombok.experimental.Accessors; import org.hisp.dhis.common.BaseIdentifiableObject; import org.hisp.dhis.common.MetadataObject; -import org.hisp.dhis.common.auth.Auth; +import org.hisp.dhis.common.auth.AuthScheme; /** * @author Morten Olav Hansen @@ -61,8 +61,10 @@ public class Route extends BaseIdentifiableObject implements MetadataObject { @JsonProperty(required = true) private Map headers = new HashMap<>(); - @JsonProperty private Auth auth; + /** Optional. Authentication to be passed as part of the route request. */ + @JsonProperty private AuthScheme auth; + /** Optional. Required authorities for invoking the route. */ @JsonProperty private List authorities = new ArrayList<>(); /** diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/AbstractAuthSchemeTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/AbstractAuthSchemeTest.java new file mode 100644 index 000000000000..7ae39360c0a1 --- /dev/null +++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/AbstractAuthSchemeTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.eventhook.targets.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.function.Function; +import org.hisp.dhis.common.auth.AuthScheme; + +public abstract class AbstractAuthSchemeTest { + + protected void assertEncrypt( + T authScheme, Function secretProvider) { + T encryptedAuthScheme = + (T) + authScheme.encrypt( + value -> { + assertEquals(secretProvider.apply(authScheme), value); + return "bar"; + }); + assertEquals("bar", secretProvider.apply(encryptedAuthScheme)); + } + + protected void assertDecrypt( + T authScheme, Function secretProvider) { + T decryptedAuthScheme = + (T) + authScheme.decrypt( + value -> { + assertEquals(secretProvider.apply(authScheme), value); + return "foo"; + }); + assertEquals("foo", secretProvider.apply(decryptedAuthScheme)); + } +} diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiHeadersAuthSchemeTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiHeadersAuthSchemeTest.java new file mode 100644 index 000000000000..c1043d1fd253 --- /dev/null +++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiHeadersAuthSchemeTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.eventhook.targets.auth; + +import java.util.Map; +import org.hisp.dhis.common.auth.ApiHeadersAuthScheme; +import org.junit.jupiter.api.Test; + +class ApiHeadersAuthSchemeTest extends AbstractAuthSchemeTest { + + @Test + void testEncrypt() { + assertEncrypt( + new ApiHeadersAuthScheme() + .setHeaders( + Map.of( + "X-API-KEY", + "T5pvst37VedtsoD70KlbumzI30Mo4pzzyAY0M6Ia8uYyPBLPeXlYzr4d3LPQD6oS")), + apiQueryParamsAuthScheme -> apiQueryParamsAuthScheme.getHeaders().get("X-API-KEY")); + } + + @Test + void testDecrypt() { + assertDecrypt( + new ApiHeadersAuthScheme() + .setHeaders( + Map.of( + "X-API-KEY", + "3PB06m2bcr0blf81OEpcIDUMUYQYHJcdQsBJyOwbmelTYBQ6fuskAGJReGgM30Cv")), + apiQueryParamsAuthScheme -> apiQueryParamsAuthScheme.getHeaders().get("X-API-KEY")); + } +} diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiQueryParamsAuthSchemeTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiQueryParamsAuthSchemeTest.java new file mode 100644 index 000000000000..7bdcb7081645 --- /dev/null +++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiQueryParamsAuthSchemeTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.eventhook.targets.auth; + +import java.util.Map; +import org.hisp.dhis.common.auth.ApiQueryParamsAuthScheme; +import org.junit.jupiter.api.Test; + +class ApiQueryParamsAuthSchemeTest extends AbstractAuthSchemeTest { + + @Test + void testEncrypt() { + assertEncrypt( + new ApiQueryParamsAuthScheme() + .setQueryParams( + Map.of( + "token", "T5pvst37VedtsoD70KlbumzI30Mo4pzzyAY0M6Ia8uYyPBLPeXlYzr4d3LPQD6oS")), + apiQueryParamsAuthScheme -> apiQueryParamsAuthScheme.getQueryParams().get("token")); + } + + @Test + void testDecrypt() { + assertDecrypt( + new ApiQueryParamsAuthScheme() + .setQueryParams( + Map.of( + "token", "3PB06m2bcr0blf81OEpcIDUMUYQYHJcdQsBJyOwbmelTYBQ6fuskAGJReGgM30Cv")), + apiQueryParamsAuthScheme -> apiQueryParamsAuthScheme.getQueryParams().get("token")); + } +} diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiTokenAuthTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiTokenAuthSchemeTest.java similarity index 71% rename from dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiTokenAuthTest.java rename to dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiTokenAuthSchemeTest.java index 49f00de5dde9..f7c42655e04a 100644 --- a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiTokenAuthTest.java +++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/ApiTokenAuthSchemeTest.java @@ -27,9 +27,11 @@ */ package org.hisp.dhis.eventhook.targets.auth; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -import org.hisp.dhis.common.auth.ApiTokenAuth; +import org.hisp.dhis.common.auth.ApiTokenAuthScheme; import org.junit.jupiter.api.Test; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -37,18 +39,34 @@ /** * @author Morten Olav Hansen */ -class ApiTokenAuthTest { +class ApiTokenAuthSchemeTest extends AbstractAuthSchemeTest { @Test void testAuthorizationHeaderSet() { - ApiTokenAuth auth = new ApiTokenAuth().setToken("90619873-3287-4296-8C22-9E1D49C0201F"); + ApiTokenAuthScheme auth = + new ApiTokenAuthScheme().setToken("90619873-3287-4296-8C22-9E1D49C0201F"); MultiValueMap headers = new LinkedMultiValueMap<>(); - auth.apply(headers); + auth.apply(headers, null); assertTrue(headers.containsKey("Authorization")); assertFalse(headers.get("Authorization").isEmpty()); assertEquals( "ApiToken 90619873-3287-4296-8C22-9E1D49C0201F", headers.get("Authorization").get(0)); } + + @Test + void testEncrypt() { + assertEncrypt( + new ApiTokenAuthScheme().setToken("90619873-3287-4296-8C22-9E1D49C0201F"), + ApiTokenAuthScheme::getToken); + } + + @Test + void testDecrypt() { + assertDecrypt( + new ApiTokenAuthScheme() + .setToken("qkQzMuVWVGw5g3WcjhYuBZXL5r2DdlURaFMTkuya2OmfhLhgf9CPdqj5wvA2JE1t"), + ApiTokenAuthScheme::getToken); + } } diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/HttpBasicAuthTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/HttpBasicAuthSchemeTest.java similarity index 70% rename from dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/HttpBasicAuthTest.java rename to dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/HttpBasicAuthSchemeTest.java index 2ecb8fbeefac..3ba4619133c7 100644 --- a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/HttpBasicAuthTest.java +++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/eventhook/targets/auth/HttpBasicAuthSchemeTest.java @@ -27,9 +27,11 @@ */ package org.hisp.dhis.eventhook.targets.auth; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -import org.hisp.dhis.common.auth.HttpBasicAuth; +import org.hisp.dhis.common.auth.HttpBasicAuthScheme; import org.junit.jupiter.api.Test; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -37,16 +39,33 @@ /** * @author Morten Olav Hansen */ -class HttpBasicAuthTest { +class HttpBasicAuthSchemeTest extends AbstractAuthSchemeTest { @Test void testAuthorizationHeaderSet() { - HttpBasicAuth auth = new HttpBasicAuth().setUsername("admin").setPassword("district"); + HttpBasicAuthScheme auth = + new HttpBasicAuthScheme().setUsername("admin").setPassword("district"); MultiValueMap headers = new LinkedMultiValueMap<>(); - auth.apply(headers); + auth.apply(headers, null); assertTrue(headers.containsKey("Authorization")); assertFalse(headers.get("Authorization").isEmpty()); assertEquals("Basic YWRtaW46ZGlzdHJpY3Q=", headers.get("Authorization").get(0)); } + + @Test + void testEncrypt() { + assertEncrypt( + new HttpBasicAuthScheme().setUsername("admin").setPassword("district"), + HttpBasicAuthScheme::getPassword); + } + + @Test + void testDecrypt() { + assertDecrypt( + new HttpBasicAuthScheme() + .setUsername("admin") + .setPassword("qkQzMuVWVGw5g3WcjhYuBZXL5r2DdlURaFMTkuya2OmfhLhgf9CPdqj5wvA2JE1t"), + HttpBasicAuthScheme::getPassword); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/route/RouteService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/route/RouteService.java index 49e57d39ed07..e9ab2b679279 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/route/RouteService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/route/RouteService.java @@ -39,17 +39,18 @@ import java.util.Optional; import java.util.function.Function; import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; -import org.hisp.dhis.common.auth.ApiTokenAuth; -import org.hisp.dhis.common.auth.Auth; -import org.hisp.dhis.common.auth.HttpBasicAuth; import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.user.User; import org.jasypt.encryption.pbe.PBEStringCleanablePasswordEncryptor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -57,6 +58,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; @@ -76,7 +79,7 @@ public class RouteService { @Qualifier(AES_128_STRING_ENCRYPTOR) private final PBEStringCleanablePasswordEncryptor encryptor; - private static final RestTemplate restTemplate = new RestTemplate(); + @Autowired @Getter @Setter private RestTemplate restTemplate; private static List allowedRequestHeaders = List.of( @@ -109,7 +112,8 @@ public class RouteService { "last-modified", "etag"); - static { + @PostConstruct + public void postConstruct() { HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setConnectionRequestTimeout(1_000); @@ -117,8 +121,8 @@ public class RouteService { requestFactory.setReadTimeout(10_000); requestFactory.setBufferRequestBody(true); - HttpClient httpClient = - HttpClientBuilder.create().disableCookieManagement().useSystemProperties().build(); + HttpClient httpClient = HttpClientBuilder.create().disableCookieManagement().build(); + requestFactory.setHttpClient(httpClient); restTemplate.setRequestFactory(requestFactory); @@ -144,12 +148,13 @@ public Route getDecryptedRoute(@Nonnull String id) { try { route = objectMapper.readValue(objectMapper.writeValueAsString(route), Route.class); } catch (JsonProcessingException ex) { - log.error( - "Unable to create clone of Route with ID " + route.getUid() + ". Please check its data."); + log.error("Unable to create clone of route: '{}'", route.getUid()); return null; } - decrypt(route); + if (route.getAuth() != null) { + route.setAuth(route.getAuth().decrypt(encryptor::decrypt)); + } return route; } @@ -169,20 +174,20 @@ public ResponseEntity exec( headers.add("X-Forwarded-User", user.getUsername()); } + MultiValueMap queryParameters = new LinkedMultiValueMap<>(); + request.getParameterMap().forEach((key, value) -> queryParameters.addAll(key, List.of(value))); + if (route.getAuth() != null) { - route.getAuth().apply(headers); + route.getAuth().apply(headers, queryParameters); } - HttpHeaders queryParameters = new HttpHeaders(); - request.getParameterMap().forEach((key, value) -> queryParameters.addAll(key, List.of(value))); - UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(route.getBaseUrl()).queryParams(queryParameters); if (subPath.isPresent()) { if (!route.allowsSubpaths()) { throw new BadRequestException( - String.format("Route %s does not allow subpaths", route.getId())); + String.format("Route '%s' does not allow sub-paths", route.getId())); } uriComponentsBuilder.path(subPath.get()); } @@ -229,7 +234,7 @@ private HttpHeaders filterHeaders( (String name) -> { String lowercaseName = name.toLowerCase(); if (!allowedHeaders.contains(lowercaseName)) { - log.debug(String.format("Blocked header %s", name)); + log.debug("Blocked header: '{}'", name); return; } List values = valuesGetter.apply(name); @@ -248,20 +253,4 @@ private HttpHeaders filterRequestHeaders(HttpServletRequest request) { private HttpHeaders filterResponseHeaders(HttpHeaders responseHeaders) { return filterHeaders(responseHeaders.keySet(), allowedResponseHeaders, responseHeaders::get); } - - private void decrypt(Route route) { - Auth auth = route.getAuth(); - - if (auth == null) { - return; - } - - if (auth.getType().equals(ApiTokenAuth.TYPE)) { - ApiTokenAuth apiTokenAuth = (ApiTokenAuth) auth; - apiTokenAuth.setToken(encryptor.decrypt(apiTokenAuth.getToken())); - } else if (auth.getType().equals(HttpBasicAuth.TYPE)) { - HttpBasicAuth httpBasicAuth = (HttpBasicAuth) auth; - httpBasicAuth.setPassword(encryptor.decrypt(httpBasicAuth.getPassword())); - } - } } diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/RouteObjectBundleHook.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/RouteObjectBundleHook.java index c4c6557ac5e5..b314ac9d64c4 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/RouteObjectBundleHook.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/RouteObjectBundleHook.java @@ -30,9 +30,7 @@ import static org.hisp.dhis.config.HibernateEncryptionConfig.AES_128_STRING_ENCRYPTOR; import lombok.AllArgsConstructor; -import org.hisp.dhis.common.auth.ApiTokenAuth; -import org.hisp.dhis.common.auth.Auth; -import org.hisp.dhis.common.auth.HttpBasicAuth; +import org.hisp.dhis.common.auth.AuthScheme; import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundle; import org.hisp.dhis.route.Route; import org.jasypt.encryption.pbe.PBEStringCleanablePasswordEncryptor; @@ -60,24 +58,20 @@ public void preUpdate(Route route, Route persistedObject, ObjectBundle bundle) { } private void encrypt(Route route) { - Auth auth = route.getAuth(); + AuthScheme auth = route.getAuth(); if (auth == null) { return; } - if (auth.getType().equals(ApiTokenAuth.TYPE)) { - ApiTokenAuth apiTokenAuth = (ApiTokenAuth) auth; - - if (StringUtils.hasText(apiTokenAuth.getToken())) { - apiTokenAuth.setToken(encryptor.encrypt(apiTokenAuth.getToken())); - } - } else if (auth.getType().equals(HttpBasicAuth.TYPE)) { - HttpBasicAuth httpBasicAuth = (HttpBasicAuth) auth; - - if (StringUtils.hasText(httpBasicAuth.getPassword())) { - httpBasicAuth.setPassword(encryptor.encrypt(httpBasicAuth.getPassword())); - } - } + route.setAuth( + auth.encrypt( + secret -> { + if (StringUtils.hasText(secret)) { + return encryptor.encrypt(secret); + } else { + return secret; + } + })); } } diff --git a/dhis-2/dhis-services/dhis-service-event-hook/src/main/java/org/hisp/dhis/eventhook/EventHookSecretManager.java b/dhis-2/dhis-services/dhis-service-event-hook/src/main/java/org/hisp/dhis/eventhook/EventHookSecretManager.java index 26463ab5327b..6a5c1d7373e1 100644 --- a/dhis-2/dhis-services/dhis-service-event-hook/src/main/java/org/hisp/dhis/eventhook/EventHookSecretManager.java +++ b/dhis-2/dhis-services/dhis-service-event-hook/src/main/java/org/hisp/dhis/eventhook/EventHookSecretManager.java @@ -31,9 +31,7 @@ import java.util.function.UnaryOperator; import lombok.RequiredArgsConstructor; -import org.hisp.dhis.common.auth.ApiTokenAuth; -import org.hisp.dhis.common.auth.Auth; -import org.hisp.dhis.common.auth.HttpBasicAuth; +import org.hisp.dhis.common.auth.AuthScheme; import org.hisp.dhis.eventhook.targets.JmsTarget; import org.hisp.dhis.eventhook.targets.KafkaTarget; import org.hisp.dhis.eventhook.targets.WebhookTarget; @@ -52,49 +50,37 @@ public class EventHookSecretManager { private final PBEStringCleanablePasswordEncryptor encryptor; public void encrypt(EventHook eventHook) { - handleSecrets(eventHook, encryptor::encrypt); + handleSecrets(eventHook, true); } public void decrypt(EventHook eventHook) { - handleSecrets(eventHook, encryptor::decrypt); + handleSecrets(eventHook, false); } - private void handleSecrets(EventHook eventHook, UnaryOperator callback) { + private void handleSecrets(EventHook eventHook, boolean encrypt) { for (Target target : eventHook.getTargets()) { if (target.getType().equals(WebhookTarget.TYPE)) { - handleWebhook((WebhookTarget) target, callback); - } else if (target.getType().equals(JmsTarget.TYPE)) { - handleJms((JmsTarget) target, callback); - } else if (target.getType().equals(KafkaTarget.TYPE)) { - handleKafka((KafkaTarget) target, callback); + handleWebhook((WebhookTarget) target, encrypt); + } else { + UnaryOperator callback = encrypt ? encryptor::encrypt : encryptor::decrypt; + if (target.getType().equals(JmsTarget.TYPE)) { + handleJms((JmsTarget) target, callback); + } else if (target.getType().equals(KafkaTarget.TYPE)) { + handleKafka((KafkaTarget) target, callback); + } } } } - private void handleWebhook(WebhookTarget target, UnaryOperator callback) { - Auth auth = target.getAuth(); - - if (auth == null) { - return; - } - - switch (auth.getType()) { - case HttpBasicAuth.TYPE: - HttpBasicAuth httpBasicAuth = (HttpBasicAuth) auth; + private void handleWebhook(WebhookTarget target, boolean encrypt) { + AuthScheme auth = target.getAuth(); - if (StringUtils.hasText(httpBasicAuth.getPassword())) { - httpBasicAuth.setPassword(callback.apply(httpBasicAuth.getPassword())); - } - break; - case ApiTokenAuth.TYPE: - ApiTokenAuth apiTokenAuth = (ApiTokenAuth) auth; - - if (StringUtils.hasText(apiTokenAuth.getToken())) { - apiTokenAuth.setToken(callback.apply(apiTokenAuth.getToken())); - } - break; - default: - break; + if (auth != null) { + if (encrypt) { + target.setAuth(auth.encrypt(encryptor::encrypt)); + } else { + target.setAuth(auth.decrypt(encryptor::decrypt)); + } } } diff --git a/dhis-2/dhis-services/dhis-service-event-hook/src/main/java/org/hisp/dhis/eventhook/handlers/WebhookHandler.java b/dhis-2/dhis-services/dhis-service-event-hook/src/main/java/org/hisp/dhis/eventhook/handlers/WebhookHandler.java index 9d4c2e01b2a5..be2bea196c6c 100644 --- a/dhis-2/dhis-services/dhis-service-event-hook/src/main/java/org/hisp/dhis/eventhook/handlers/WebhookHandler.java +++ b/dhis-2/dhis-services/dhis-service-event-hook/src/main/java/org/hisp/dhis/eventhook/handlers/WebhookHandler.java @@ -39,8 +39,11 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; /** * @author Morten Olav Hansen @@ -63,21 +66,27 @@ public void run(EventHook eventHook, Event event, String payload) { httpHeaders.setContentType(MediaType.parseMediaType(webhookTarget.getContentType())); httpHeaders.setAll(webhookTarget.getHeaders()); + MultiValueMap queryParams = new LinkedMultiValueMap<>(); if (webhookTarget.getAuth() != null) { - webhookTarget.getAuth().apply(httpHeaders); + webhookTarget.getAuth().apply(httpHeaders, queryParams); } HttpEntity httpEntity = new HttpEntity<>(payload, httpHeaders); + String webhookUri = + new DefaultUriBuilderFactory(webhookTarget.getUrl()) + .builder() + .queryParams(queryParams) + .build() + .toString(); try { ResponseEntity response = - restTemplate.postForEntity(webhookTarget.getUrl(), httpEntity, String.class); + restTemplate.postForEntity(webhookUri, httpEntity, String.class); log.info( - "EventHook '{}' response status '{}' and body: {}", + "EventHook '{}' response status '{}'", eventHook.getUid(), - response.getStatusCode().name(), - response.getBody()); + response.getStatusCode().name()); } catch (RestClientException ex) { log.error(ex.getMessage()); } diff --git a/dhis-2/dhis-services/dhis-service-field-filtering/src/main/java/org/hisp/dhis/fieldfiltering/FieldFilterSimpleBeanPropertyFilter.java b/dhis-2/dhis-services/dhis-service-field-filtering/src/main/java/org/hisp/dhis/fieldfiltering/FieldFilterSimpleBeanPropertyFilter.java index d0086a2fb9ed..d59722dc6310 100644 --- a/dhis-2/dhis-services/dhis-service-field-filtering/src/main/java/org/hisp/dhis/fieldfiltering/FieldFilterSimpleBeanPropertyFilter.java +++ b/dhis-2/dhis-services/dhis-service-field-filtering/src/main/java/org/hisp/dhis/fieldfiltering/FieldFilterSimpleBeanPropertyFilter.java @@ -43,8 +43,10 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.hisp.dhis.common.SystemDefaultMetadataObject; -import org.hisp.dhis.common.auth.ApiTokenAuth; -import org.hisp.dhis.common.auth.HttpBasicAuth; +import org.hisp.dhis.common.auth.ApiHeadersAuthScheme; +import org.hisp.dhis.common.auth.ApiQueryParamsAuthScheme; +import org.hisp.dhis.common.auth.ApiTokenAuthScheme; +import org.hisp.dhis.common.auth.HttpBasicAuthScheme; import org.hisp.dhis.eventhook.targets.JmsTarget; import org.hisp.dhis.eventhook.targets.KafkaTarget; import org.hisp.dhis.scheduling.JobParameters; @@ -74,8 +76,10 @@ public class FieldFilterSimpleBeanPropertyFilter extends SimpleBeanPropertyFilte */ private static final Map, Set> IGNORE_LIST = Map.of( - HttpBasicAuth.class, Set.of("auth.password", "targets.auth.password"), - ApiTokenAuth.class, Set.of("auth.token", "targets.auth.token"), + HttpBasicAuthScheme.class, Set.of("auth.password", "targets.auth.password"), + ApiTokenAuthScheme.class, Set.of("auth.token", "targets.auth.token"), + ApiHeadersAuthScheme.class, Set.of("auth.headers", "targets.auth.headers"), + ApiQueryParamsAuthScheme.class, Set.of("auth.queryParams", "targets.auth.queryParams"), JmsTarget.class, Set.of("targets.password"), KafkaTarget.class, Set.of("targets.password")); diff --git a/dhis-2/dhis-support/dhis-support-hibernate/src/main/resources/org/hisp/dhis/usertype/UserTypes.hbm.xml b/dhis-2/dhis-support/dhis-support-hibernate/src/main/resources/org/hisp/dhis/usertype/UserTypes.hbm.xml index 301eb36e2e58..51fe9e0bb06b 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/src/main/resources/org/hisp/dhis/usertype/UserTypes.hbm.xml +++ b/dhis-2/dhis-support/dhis-support-hibernate/src/main/resources/org/hisp/dhis/usertype/UserTypes.hbm.xml @@ -188,7 +188,7 @@ - org.hisp.dhis.common.auth.Auth + org.hisp.dhis.common.auth.AuthScheme diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/EventHookControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/EventHookControllerTest.java index 301a5be8be52..a02988c3c515 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/EventHookControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/EventHookControllerTest.java @@ -33,8 +33,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.hisp.dhis.common.auth.ApiTokenAuth; -import org.hisp.dhis.common.auth.HttpBasicAuth; +import org.hisp.dhis.common.auth.ApiTokenAuthScheme; +import org.hisp.dhis.common.auth.HttpBasicAuthScheme; import org.hisp.dhis.eventhook.targets.WebhookTarget; import org.hisp.dhis.jsontree.JsonList; import org.hisp.dhis.jsontree.JsonObject; @@ -80,7 +80,7 @@ void testCreateEventHookWebhookApiToken() { JsonObject auth = target.getObject("auth"); assertFalse(auth.has("token")); - assertEquals(ApiTokenAuth.TYPE, auth.getString("type").string()); + assertEquals(ApiTokenAuthScheme.API_TOKEN_TYPE, auth.getString("type").string()); } @Test @@ -107,7 +107,7 @@ void testCreateEventHookWebhookHttpBasic() { JsonObject auth = target.getObject("auth"); assertTrue(auth.has("type", "username")); assertFalse(auth.has("password")); - assertEquals(HttpBasicAuth.TYPE, auth.getString("type").string()); + assertEquals(HttpBasicAuthScheme.HTTP_BASIC_TYPE, auth.getString("type").string()); assertEquals("admin", auth.getString("username").string()); } @@ -136,7 +136,7 @@ void testCreateEventHookWebhookHttpBasicDefaultEnabled() { JsonObject auth = target.getObject("auth"); assertTrue(auth.has("type", "username")); assertFalse(auth.has("password")); - assertEquals(HttpBasicAuth.TYPE, auth.getString("type").string()); + assertEquals(HttpBasicAuthScheme.HTTP_BASIC_TYPE, auth.getString("type").string()); assertEquals("admin", auth.getString("username").string()); } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/RouteControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/RouteControllerTest.java new file mode 100644 index 000000000000..391e46868fe7 --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/RouteControllerTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.controller; + +import static org.hisp.dhis.web.WebClientUtils.assertStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import org.hisp.dhis.common.auth.ApiHeadersAuthScheme; +import org.hisp.dhis.common.auth.ApiQueryParamsAuthScheme; +import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.jsontree.JsonString; +import org.hisp.dhis.route.Route; +import org.hisp.dhis.route.RouteService; +import org.hisp.dhis.webapi.DhisControllerIntegrationTest; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +@Transactional +class RouteControllerTest extends DhisControllerIntegrationTest { + + @Autowired private RouteService service; + + @Autowired private ObjectMapper jsonMapper; + + @Test + void testRunRouteGivenApiQueryParamsAuthScheme() + throws JsonProcessingException, MalformedURLException { + ArgumentCaptor urlArgumentCaptor = ArgumentCaptor.forClass(String.class); + + RestTemplate mockRestTemplate = mock(RestTemplate.class); + when(mockRestTemplate.exchange( + urlArgumentCaptor.capture(), + any(HttpMethod.class), + any(HttpEntity.class), + any(Class.class))) + .thenReturn( + new ResponseEntity<>( + jsonMapper.writeValueAsString(Map.of("name", "John Doe")), HttpStatus.OK)); + service.setRestTemplate(mockRestTemplate); + + Map route = new HashMap<>(); + route.put("name", "route-under-test"); + route.put("auth", Map.of("type", "api-query-params", "queryParams", Map.of("token", "foo"))); + route.put("url", "http://stub"); + + HttpResponse postHttpResponse = POST("/routes", jsonMapper.writeValueAsString(route)); + HttpResponse runHttpResponse = + GET( + "/routes/{id}/run", + postHttpResponse.content().get("response.uid").as(JsonString.class).string()); + assertStatus(org.hisp.dhis.web.HttpStatus.OK, runHttpResponse); + assertEquals("John Doe", runHttpResponse.content().get("name").as(JsonString.class).string()); + + assertEquals("token=foo", new URL(urlArgumentCaptor.getValue()).getQuery()); + } + + @Test + void testRunRouteGivenApiHeadersAuthScheme() throws JsonProcessingException { + ArgumentCaptor> httpEntityArgumentCaptor = + ArgumentCaptor.forClass(HttpEntity.class); + + RestTemplate mockRestTemplate = mock(RestTemplate.class); + when(mockRestTemplate.exchange( + anyString(), + any(HttpMethod.class), + httpEntityArgumentCaptor.capture(), + any(Class.class))) + .thenReturn( + new ResponseEntity<>( + jsonMapper.writeValueAsString(Map.of("name", "John Doe")), HttpStatus.OK)); + service.setRestTemplate(mockRestTemplate); + + Map route = new HashMap<>(); + route.put("name", "route-under-test"); + route.put("auth", Map.of("type", "api-headers", "headers", Map.of("X-API-KEY", "foo"))); + route.put("url", "http://stub"); + + HttpResponse postHttpResponse = POST("/routes", jsonMapper.writeValueAsString(route)); + HttpResponse runHttpResponse = + GET( + "/routes/{id}/run", + postHttpResponse.content().get("response.uid").as(JsonString.class).string()); + assertStatus(org.hisp.dhis.web.HttpStatus.OK, runHttpResponse); + assertEquals("John Doe", runHttpResponse.content().get("name").as(JsonString.class).string()); + + HttpEntity capturedHttpEntity = httpEntityArgumentCaptor.getValue(); + HttpHeaders headers = capturedHttpEntity.getHeaders(); + assertEquals("foo", headers.get("X-API-KEY").get(0)); + } + + @Test + void testAddRouteGivenApiHeadersAuthScheme() throws JsonProcessingException { + Map route = new HashMap<>(); + route.put("name", "route-under-test"); + route.put("auth", Map.of("type", "api-headers", "headers", Map.of("X-API-KEY", "foo"))); + route.put("url", "http://stub"); + + HttpResponse postHttpResponse = POST("/routes", jsonMapper.writeValueAsString(route)); + assertStatus(org.hisp.dhis.web.HttpStatus.CREATED, postHttpResponse); + + HttpResponse getHttpResponse = + GET( + "/routes/{id}", + postHttpResponse.content().get("response.uid").as(JsonString.class).string()); + assertStatus(org.hisp.dhis.web.HttpStatus.OK, getHttpResponse); + assertNotEquals( + "foo", + getHttpResponse.content().get("auth.headers.X-API-KEY").as(JsonString.class).string()); + assertEquals( + ApiHeadersAuthScheme.API_HEADERS_TYPE, + getHttpResponse.content().get("auth.type").as(JsonString.class).string()); + } + + @Test + void testGetRouteGivenApiHeadersAuthScheme() throws JsonProcessingException { + Map route = new HashMap<>(); + route.put("name", "route-under-test"); + route.put("auth", Map.of("type", "api-headers", "headers", Map.of("X-API-KEY", "foo"))); + route.put("url", "http://stub"); + + HttpResponse postHttpResponse = POST("/routes", jsonMapper.writeValueAsString(route)); + + HttpResponse getHttpResponse = + GET( + "/routes/{id}", + postHttpResponse.content().get("response.uid").as(JsonString.class).string()); + assertStatus(org.hisp.dhis.web.HttpStatus.OK, getHttpResponse); + assertNotEquals( + "foo", + getHttpResponse.content().get("auth.headers.X-API-KEY").as(JsonString.class).string()); + assertEquals( + ApiHeadersAuthScheme.API_HEADERS_TYPE, + getHttpResponse.content().get("auth.type").as(JsonString.class).string()); + assertFalse(getHttpResponse.content().get("auth").as(JsonObject.class).has("headers")); + } + + @Test + void testAddRouteGivenApiQueryParamsAuthScheme() throws JsonProcessingException { + ApiQueryParamsAuthScheme queryParamsAuthScheme = new ApiQueryParamsAuthScheme(); + queryParamsAuthScheme.setQueryParams(Map.of("token", "foo")); + + Route route = new Route(); + route.setName("route-under-test"); + route.setAuth(queryParamsAuthScheme); + route.setUrl("http://stub"); + + HttpResponse postHttpResponse = POST("/routes", jsonMapper.writeValueAsString(route)); + assertStatus(org.hisp.dhis.web.HttpStatus.CREATED, postHttpResponse); + + HttpResponse getHttpResponse = + GET( + "/routes/{id}", + postHttpResponse.content().get("response.uid").as(JsonString.class).string()); + assertStatus(org.hisp.dhis.web.HttpStatus.OK, getHttpResponse); + assertNotEquals( + "foo", getHttpResponse.content().get("auth.headers.token").as(JsonString.class).string()); + assertEquals( + ApiQueryParamsAuthScheme.API_QUERY_PARAMS_TYPE, + getHttpResponse.content().get("auth.type").as(JsonString.class).string()); + } + + @Test + void testGetRouteGivenApiQueryParamsAuthScheme() throws JsonProcessingException { + Map route = new HashMap<>(); + route.put("name", "route-under-test"); + route.put("auth", Map.of("type", "api-query-params", "queryParams", Map.of("token", "foo"))); + route.put("url", "http://stub"); + + HttpResponse postHttpResponse = POST("/routes", jsonMapper.writeValueAsString(route)); + + HttpResponse getHttpResponse = + GET( + "/routes/{id}", + postHttpResponse.content().get("response.uid").as(JsonString.class).string()); + assertStatus(org.hisp.dhis.web.HttpStatus.OK, getHttpResponse); + assertEquals( + ApiQueryParamsAuthScheme.API_QUERY_PARAMS_TYPE, + getHttpResponse.content().get("auth.type").as(JsonString.class).string()); + assertFalse(getHttpResponse.content().get("auth").as(JsonObject.class).has("queryParams")); + } +} From c8dcddc9d3d41d6d652502789abf3046f0ef0678 Mon Sep 17 00:00:00 2001 From: cjmamo <823038+cjmamo@users.noreply.github.com> Date: Mon, 3 Feb 2025 09:51:22 +0100 Subject: [PATCH 2/2] test: fix failing assertion caused by environment changes --- .../webapi/controller/AppHubControllerTest.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AppHubControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AppHubControllerTest.java index 02bfdca0adff..b886e9fd1476 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AppHubControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AppHubControllerTest.java @@ -27,13 +27,16 @@ */ package org.hisp.dhis.webapi.controller; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.hisp.dhis.jsontree.JsonArray; +import org.hisp.dhis.jsontree.JsonResponse; import org.hisp.dhis.web.HttpStatus; import org.hisp.dhis.webapi.DhisControllerConvenienceTest; +import org.hisp.dhis.webapi.json.domain.JsonWebMessage; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -64,12 +67,13 @@ void testListAppHub_ClientError() { configuration .getProperties() .setProperty(ConfigurationKey.APPHUB_API_URL.getKey(), "http://localhost/doesnotwork"); - assertWebMessage( - "Service Unavailable", - 503, - "ERROR", - "I/O error on GET request for \"http://localhost/doesnotwork/apps\": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused)", - GET("/appHub").content(HttpStatus.SERVICE_UNAVAILABLE)); + + JsonResponse jsonResponse = GET("/appHub").content(HttpStatus.SERVICE_UNAVAILABLE); + JsonWebMessage jsonWebMessage = jsonResponse.as(JsonWebMessage.class); + + assertEquals(503, jsonWebMessage.getHttpStatusCode()); + assertEquals("ERROR", jsonWebMessage.getStatus()); + assertTrue(jsonWebMessage.getMessage().startsWith("I/O error on GET request for ")); } @Test