diff --git a/bundle/mvn/default-bundle/pom.xml b/bundle/mvn/default-bundle/pom.xml index 20f1ef5422..5828127f84 100644 --- a/bundle/mvn/default-bundle/pom.xml +++ b/bundle/mvn/default-bundle/pom.xml @@ -60,6 +60,10 @@ io.camunda.connector connector-microsoft-teams + + io.camunda.connector + connector-graphql + diff --git a/bundle/mvn/pom.xml b/bundle/mvn/pom.xml index 0403b4fa9b..e08fe64b5c 100644 --- a/bundle/mvn/pom.xml +++ b/bundle/mvn/pom.xml @@ -74,6 +74,11 @@ connector-slack ${project.version} + + io.camunda.connector + connector-graphql + ${project.version} + diff --git a/connector-archetype-internal/src/main/resources/archetype-resources/pom.xml b/connector-archetype-internal/src/main/resources/archetype-resources/pom.xml index 025a656a07..6761ab5e81 100644 --- a/connector-archetype-internal/src/main/resources/archetype-resources/pom.xml +++ b/connector-archetype-internal/src/main/resources/archetype-resources/pom.xml @@ -11,7 +11,7 @@ ${artifactId} - ${connectorName} cnnector for Camunda 8 + ${connectorName} connector for Camunda 8 ${artifactId} jar diff --git a/connectors/connectors-common-library/pom.xml b/connectors/connectors-common-library/pom.xml new file mode 100644 index 0000000000..9404a9c4e4 --- /dev/null +++ b/connectors/connectors-common-library/pom.xml @@ -0,0 +1,67 @@ + + 4.0.0 + + + io.camunda.connector + connector-function-parent + 0.16.0-SNAPSHOT + ../pom.xml + + + connectors-common-library + Common library for connectors + connectors-common-library + jar + + + Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH +under one or more contributor license agreements. See the NOTICE file +distributed with this work for additional information regarding copyright +ownership. Camunda licenses this file to you under the Apache License, +Version 2.0; 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. + + + + + org.danilopianini + gson-extras + + + + com.google.http-client + google-http-client-gson + + + + com.google.http-client + google-http-client-apache-v2 + + + + com.google.auth + google-auth-library-oauth2-http + + + + org.apache.httpcomponents + httpcore + + + + org.slf4j + jcl-over-slf4j + + + + \ No newline at end of file diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/Authentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/Authentication.java similarity index 97% rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/Authentication.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/Authentication.java index ff37b792ab..20086a80c5 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/Authentication.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/Authentication.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.auth; +package io.camunda.connector.common.auth; import com.google.api.client.http.HttpHeaders; import com.google.common.base.Objects; diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/BasicAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BasicAuthentication.java similarity index 98% rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/BasicAuthentication.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BasicAuthentication.java index ac8ee62bc6..91a10b5bad 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/BasicAuthentication.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BasicAuthentication.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.auth; +package io.camunda.connector.common.auth; import com.google.api.client.http.HttpHeaders; import com.google.common.base.Objects; diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/BearerAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BearerAuthentication.java similarity index 97% rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/BearerAuthentication.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BearerAuthentication.java index a82cf35a38..7c95ef2f91 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/BearerAuthentication.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BearerAuthentication.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.auth; +package io.camunda.connector.common.auth; import com.google.api.client.http.HttpHeaders; import com.google.common.base.Objects; diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/CustomAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/CustomAuthentication.java similarity index 91% rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/CustomAuthentication.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/CustomAuthentication.java index 4783f6ebc9..29dd2f40d8 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/CustomAuthentication.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/CustomAuthentication.java @@ -14,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.auth; +package io.camunda.connector.common.auth; import com.google.api.client.http.HttpHeaders; import io.camunda.connector.api.annotation.Secret; -import io.camunda.connector.http.model.HttpJsonRequest; +import io.camunda.connector.common.model.CommonRequest; import java.util.Map; import java.util.Objects; import javax.validation.Valid; @@ -26,7 +26,7 @@ public class CustomAuthentication extends Authentication { - @NotNull @Valid private HttpJsonRequest request; + @NotNull @Valid private CommonRequest request; @Secret private Map outputBody; @@ -35,11 +35,11 @@ public class CustomAuthentication extends Authentication { @Override public void setHeaders(final HttpHeaders headers) {} - public HttpJsonRequest getRequest() { + public CommonRequest getRequest() { return request; } - public void setRequest(final HttpJsonRequest request) { + public void setRequest(final CommonRequest request) { this.request = request; } diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/NoAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/NoAuthentication.java similarity index 96% rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/NoAuthentication.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/NoAuthentication.java index 5a84625b60..bd8b5ce061 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/NoAuthentication.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/NoAuthentication.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.auth; +package io.camunda.connector.common.auth; import com.google.api.client.http.HttpHeaders; diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/OAuthAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/OAuthAuthentication.java similarity index 86% rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/OAuthAuthentication.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/OAuthAuthentication.java index af3c9a9d82..ea3370db58 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/OAuthAuthentication.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/OAuthAuthentication.java @@ -14,13 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.auth; +package io.camunda.connector.common.auth; import com.google.api.client.http.HttpHeaders; import io.camunda.connector.api.annotation.Secret; -import io.camunda.connector.http.constants.Constants; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import javax.validation.constraints.NotEmpty; @@ -31,20 +28,8 @@ public class OAuthAuthentication extends Authentication { @NotEmpty @Secret private String clientSecret; @Secret private String audience; @NotEmpty private String clientAuthentication; - private String scopes; - - public Map getDataForAuthRequestBody() { - Map data = new HashMap<>(); - data.put(Constants.GRANT_TYPE, grantType); - data.put(Constants.AUDIENCE, audience); - data.put(Constants.SCOPE, scopes); - if (clientAuthentication.equals(Constants.CREDENTIALS_BODY)) { - data.put(Constants.CLIENT_ID, clientId); - data.put(Constants.CLIENT_SECRET, clientSecret); - } - return data; - } + private String scopes; public String getClientId() { return clientId; diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/constants/Constants.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/constants/Constants.java similarity index 93% rename from connectors/http-json/src/main/java/io/camunda/connector/http/constants/Constants.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/constants/Constants.java index 0f29108410..552e88b468 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/constants/Constants.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/constants/Constants.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.constants; +package io.camunda.connector.common.constants; public class Constants { public static final String GRANT_TYPE = "grant_type"; @@ -23,7 +23,6 @@ public class Constants { public static final String AUDIENCE = "audience"; public static final String SCOPE = "scope"; public static final String ACCESS_TOKEN = "access_token"; - public static final String COMPUTE_METADATA = "computeMetadata"; public static final String BASIC_AUTH_HEADER = "basicAuthHeader"; public static final String CREDENTIALS_BODY = "credentialsBody"; public static final String PROXY_FUNCTION_URL_ENV_NAME = "CAMUNDA_CONNECTOR_HTTP_PROXY_URL"; diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonRequest.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonRequest.java new file mode 100644 index 0000000000..6a42de90d9 --- /dev/null +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonRequest.java @@ -0,0 +1,162 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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 io.camunda.connector.common.model; + +import io.camunda.connector.api.annotation.Secret; +import io.camunda.connector.common.auth.Authentication; +import java.util.Map; +import java.util.Objects; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +public class CommonRequest { + + @NotBlank + @Pattern(regexp = "^(http://|https://|secrets).*$") + @Secret + private String url; + + @NotBlank @Secret private String method; + + @Valid @Secret private Authentication authentication; + + @Pattern(regexp = "^([0-9]*$)|(secrets.*$)") + @Secret + private String connectionTimeoutInSeconds; + + @Secret private Map headers; + + @Secret private Object body; + + @Secret private Map queryParameters; + + public Object getBody() { + return body; + } + + public void setBody(final Object body) { + this.body = body; + } + + public boolean hasHeaders() { + return headers != null; + } + + public boolean hasBody() { + return body != null; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(final Map headers) { + this.headers = headers; + } + + public boolean hasQueryParameters() { + return queryParameters != null; + } + + public Map getQueryParameters() { + return queryParameters; + } + + public void setQueryParameters(Map queryParameters) { + this.queryParameters = queryParameters; + } + + public boolean hasAuthentication() { + return authentication != null; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Authentication getAuthentication() { + return authentication; + } + + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getConnectionTimeoutInSeconds() { + return connectionTimeoutInSeconds; + } + + public void setConnectionTimeoutInSeconds(String connectionTimeoutInSeconds) { + this.connectionTimeoutInSeconds = connectionTimeoutInSeconds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CommonRequest that = (CommonRequest) o; + return url.equals(that.url) + && method.equals(that.method) + && Objects.equals(authentication, that.authentication) + && Objects.equals(connectionTimeoutInSeconds, that.connectionTimeoutInSeconds) + && Objects.equals(headers, that.headers) + && Objects.equals(body, that.body) + && Objects.equals(queryParameters, that.queryParameters); + } + + @Override + public int hashCode() { + return Objects.hash( + url, method, authentication, connectionTimeoutInSeconds, headers, body, queryParameters); + } + + @Override + public String toString() { + return "CommonRequest{" + + "url='" + + url + + '\'' + + ", method='" + + method + + '\'' + + ", authentication=" + + authentication + + ", connectionTimeoutInSeconds='" + + connectionTimeoutInSeconds + + '\'' + + ", headers=" + + headers + + ", body=" + + body + + ", queryParameters=" + + queryParameters + + '}'; + } +} diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonResult.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonResult.java new file mode 100644 index 0000000000..3e3fee0f62 --- /dev/null +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonResult.java @@ -0,0 +1,75 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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 io.camunda.connector.common.model; + +import com.google.common.base.Objects; +import java.util.Map; + +public class CommonResult { + + private int status; + private Map headers; + private Object body; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public Object getBody() { + return body; + } + + public void setBody(Object body) { + this.body = body; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CommonResult that = (CommonResult) o; + return status == that.status + && Objects.equal(headers, that.headers) + && Objects.equal(body, that.body); + } + + @Override + public int hashCode() { + return Objects.hashCode(status, headers, body); + } + + @Override + public String toString() { + return "GraphQLResult{" + "status=" + status + ", headers=" + headers + ", body=" + body + '}'; + } +} diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/model/ErrorResponse.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/ErrorResponse.java similarity index 97% rename from connectors/http-json/src/main/java/io/camunda/connector/http/model/ErrorResponse.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/ErrorResponse.java index 8f6285f435..9029e14cf7 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/model/ErrorResponse.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/ErrorResponse.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.model; +package io.camunda.connector.common.model; import java.util.Objects; diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpRequestBuilder.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/HttpRequestBuilder.java similarity index 98% rename from connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpRequestBuilder.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/HttpRequestBuilder.java index 6790ff42ac..b904c2b091 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpRequestBuilder.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/HttpRequestBuilder.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.model; +package io.camunda.connector.common.model; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpContent; diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/AuthenticationService.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/AuthenticationService.java new file mode 100644 index 0000000000..733bd7820f --- /dev/null +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/AuthenticationService.java @@ -0,0 +1,127 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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 io.camunda.connector.common.services; + +import static io.camunda.connector.common.utils.Timeout.setTimeout; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.UrlEncodedContent; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.camunda.connector.common.auth.CustomAuthentication; +import io.camunda.connector.common.auth.OAuthAuthentication; +import io.camunda.connector.common.constants.Constants; +import io.camunda.connector.common.model.CommonRequest; +import io.camunda.connector.common.utils.JsonHelper; +import io.camunda.connector.common.utils.ResponseParser; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AuthenticationService { + + private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationService.class); + + private final Gson gson; + private final HttpRequestFactory requestFactory; + + public AuthenticationService(final Gson gson, final HttpRequestFactory requestFactory) { + this.gson = gson; + this.requestFactory = requestFactory; + } + + public String extractOAuthAccessToken(HttpResponse oauthResponse) throws IOException { + return Optional.ofNullable(JsonHelper.getAsJsonElement(oauthResponse.parseAsString(), gson)) + .map(JsonElement::getAsJsonObject) + .map(jsonObject -> jsonObject.get(Constants.ACCESS_TOKEN)) + .map(JsonElement::getAsString) + .orElse(null); + } + + public void fillRequestFromCustomAuthResponseData( + final CommonRequest request, + final CustomAuthentication authentication, + final HttpResponse httpResponse) + throws IOException { + String strResponse = httpResponse.parseAsString(); + Map headers = + ResponseParser.extractPropertiesFromBody( + authentication.getOutputHeaders(), strResponse, gson); + if (headers != null) { + if (!request.hasHeaders()) { + request.setHeaders(new HashMap<>()); + } + request.getHeaders().putAll(headers); + } + + Map body = + ResponseParser.extractPropertiesFromBody(authentication.getOutputBody(), strResponse, gson); + if (body != null) { + if (!request.hasBody()) { + request.setBody(new Object()); + } + JsonObject requestBody = gson.toJsonTree(request.getBody()).getAsJsonObject(); + // for now, we can add only string property to body, example of this object : + // "{"key":"value"}" but we can expand this method + body.forEach(requestBody::addProperty); + request.setBody(gson.fromJson(gson.toJson(requestBody), Object.class)); + } + } + + public HttpRequest createOAuthRequest(CommonRequest request) throws IOException { + OAuthAuthentication authentication = (OAuthAuthentication) request.getAuthentication(); + + final GenericUrl genericUrl = new GenericUrl(authentication.getOauthTokenEndpoint()); + Map data = getDataForAuthRequestBody(authentication); + HttpContent content = new UrlEncodedContent(data); + final String method = Constants.POST; + final var httpRequest = requestFactory.buildRequest(method, genericUrl, content); + httpRequest.setFollowRedirects(false); + setTimeout(request, httpRequest); + HttpHeaders headers = new HttpHeaders(); + + if (Constants.BASIC_AUTH_HEADER.equals(authentication.getClientAuthentication())) { + headers.setBasicAuthentication( + authentication.getClientId(), authentication.getClientSecret()); + } + headers.setContentType(Constants.APPLICATION_X_WWW_FORM_URLENCODED); + httpRequest.setHeaders(headers); + return httpRequest; + } + + private static Map getDataForAuthRequestBody(OAuthAuthentication authentication) { + Map data = new HashMap<>(); + data.put(Constants.GRANT_TYPE, authentication.getGrantType()); + data.put(Constants.AUDIENCE, authentication.getAudience()); + data.put(Constants.SCOPE, authentication.getScopes()); + + if (Constants.CREDENTIALS_BODY.equals(authentication.getClientAuthentication())) { + data.put(Constants.CLIENT_ID, authentication.getClientId()); + data.put(Constants.CLIENT_SECRET, authentication.getClientSecret()); + } + return data; + } +} diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPProxyService.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPProxyService.java new file mode 100644 index 0000000000..cb423d59b0 --- /dev/null +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPProxyService.java @@ -0,0 +1,63 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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 io.camunda.connector.common.services; + +import com.google.api.client.http.AbstractHttpContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.gson.Gson; +import io.camunda.connector.common.constants.Constants; +import io.camunda.connector.common.model.CommonRequest; +import io.camunda.connector.common.model.HttpRequestBuilder; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public final class HTTPProxyService { + + public static HttpRequest toRequestViaProxy( + final Gson gson, + final HttpRequestFactory requestFactory, + final CommonRequest request, + final String proxyFunctionUrl) + throws IOException { + // Using the JsonHttpContent cannot work with an element on the root content, + // hence write it ourselves: + final String contentAsJson = gson.toJson(request); + HttpContent content = + new AbstractHttpContent(Constants.APPLICATION_JSON_CHARSET_UTF_8) { + public void writeTo(OutputStream outputStream) throws IOException { + outputStream.write(contentAsJson.getBytes(StandardCharsets.UTF_8)); + } + }; + + HttpRequest httpRequest = + new HttpRequestBuilder() + .method(Constants.POST) + .genericUrl(new GenericUrl(proxyFunctionUrl)) + .content(content) + .connectionTimeoutInSeconds(request.getConnectionTimeoutInSeconds()) + .followRedirects(false) + .build(requestFactory); + + ProxyOAuthHelper.addOauthHeaders( + httpRequest, ProxyOAuthHelper.initializeCredentials(proxyFunctionUrl)); + return httpRequest; + } +} diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPService.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPService.java new file mode 100644 index 0000000000..381db705cc --- /dev/null +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPService.java @@ -0,0 +1,118 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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 io.camunda.connector.common.services; + +import static org.apache.http.entity.ContentType.APPLICATION_JSON; + +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.gson.Gson; +import io.camunda.connector.api.error.ConnectorException; +import io.camunda.connector.common.constants.Constants; +import io.camunda.connector.common.model.CommonRequest; +import io.camunda.connector.common.model.CommonResult; +import io.camunda.connector.common.model.ErrorResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HTTPService { + + private static final Logger LOGGER = LoggerFactory.getLogger(HTTPService.class); + + private final Gson gson; + + public HTTPService(final Gson gson) { + this.gson = gson; + } + + public HttpHeaders createHeaders(final CommonRequest request, String bearerToken) { + final HttpHeaders httpHeaders = new HttpHeaders(); + if (Constants.POST.equalsIgnoreCase(request.getMethod())) { + httpHeaders.setContentType(APPLICATION_JSON.getMimeType()); + } + if (request.hasAuthentication()) { + if (bearerToken != null && !bearerToken.isEmpty()) { + httpHeaders.setAuthorization("Bearer " + bearerToken); + } + request.getAuthentication().setHeaders(httpHeaders); + } + return httpHeaders; + } + + public HttpResponse executeHttpRequest(HttpRequest externalRequest) throws IOException { + return executeHttpRequest(externalRequest, false); + } + + public HttpResponse executeHttpRequest(HttpRequest externalRequest, boolean isProxyCall) + throws IOException { + try { + return externalRequest.execute(); + } catch (HttpResponseException hrex) { + var errorCode = String.valueOf(hrex.getStatusCode()); + var errorMessage = hrex.getMessage(); + if (isProxyCall && hrex.getContent() != null) { + try { + final var errorContent = gson.fromJson(hrex.getContent(), ErrorResponse.class); + errorCode = errorContent.getErrorCode(); + errorMessage = errorContent.getError(); + } catch (Exception e) { + // cannot be loaded as JSON, ignore and use plain message + LOGGER.warn("Error response cannot be parsed as JSON! Will use the plain message."); + } + } + throw new ConnectorException(errorCode, errorMessage, hrex); + } + } + + public T toHttpJsonResponse( + final HttpResponse externalResponse, final Class resultClass) + throws InstantiationException, IllegalAccessException { + T connectorResult = resultClass.newInstance(); + connectorResult.setStatus(externalResponse.getStatusCode()); + final Map headers = new HashMap<>(); + externalResponse + .getHeaders() + .forEach( + (k, v) -> { + if (v instanceof List && ((List) v).size() == 1) { + headers.put(k, ((List) v).get(0)); + } else { + headers.put(k, v); + } + }); + connectorResult.setHeaders(headers); + try (InputStream content = externalResponse.getContent(); + Reader reader = new InputStreamReader(content)) { + final Object body = gson.fromJson(reader, Object.class); + if (body != null) { + connectorResult.setBody(body); + } + } catch (final Exception e) { + LOGGER.error("Failed to parse external response: {}", externalResponse, e); + } + return connectorResult; + } +} diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/ProxyOAuthHelper.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/ProxyOAuthHelper.java similarity index 98% rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/ProxyOAuthHelper.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/ProxyOAuthHelper.java index 030049cff1..a71058258f 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/ProxyOAuthHelper.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/ProxyOAuthHelper.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http.auth; +package io.camunda.connector.common.services; import com.google.api.client.http.HttpRequest; import com.google.auth.oauth2.GoogleCredentials; diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/JsonHelper.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/JsonHelper.java new file mode 100644 index 0000000000..6c7f71af00 --- /dev/null +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/JsonHelper.java @@ -0,0 +1,31 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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 io.camunda.connector.common.utils; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import java.util.Optional; + +public class JsonHelper { + + public static JsonElement getAsJsonElement(final String strResponse, final Gson gson) { + return Optional.ofNullable(strResponse) + .filter(response -> !response.isBlank()) + .map(response -> gson.fromJson(response, JsonElement.class)) + .orElse(null); + } +} diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/ResponseParser.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/ResponseParser.java similarity index 76% rename from connectors/http-json/src/main/java/io/camunda/connector/http/ResponseParser.java rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/ResponseParser.java index 27367fe138..40007d2def 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/ResponseParser.java +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/ResponseParser.java @@ -14,40 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.camunda.connector.http; +package io.camunda.connector.common.utils; -import com.google.api.client.http.HttpResponse; import com.google.gson.Gson; import com.google.gson.JsonElement; -import io.camunda.connector.http.components.GsonComponentSupplier; -import io.camunda.connector.http.constants.Constants; -import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Optional; -public final class ResponseParser { - - private static final Gson gson = GsonComponentSupplier.gsonInstance(); - - private ResponseParser() {} - - public static String extractOAuthAccessToken(HttpResponse oauthResponse) throws IOException { - return Optional.ofNullable(getAsJsonElement(oauthResponse.parseAsString())) - .map(JsonElement::getAsJsonObject) - .map(jsonObject -> jsonObject.get(Constants.ACCESS_TOKEN)) - .map(JsonElement::getAsString) - .orElse(null); - } +public class ResponseParser { public static Map extractPropertiesFromBody( - final Map requestedProperties, final String strResponse) { + final Map requestedProperties, final String strResponse, final Gson gson) { if (requestedProperties == null || requestedProperties.isEmpty()) { return null; } final JsonElement asJsonElement = - Optional.ofNullable(getAsJsonElement(strResponse)) + Optional.ofNullable(JsonHelper.getAsJsonElement(strResponse, gson)) .orElseThrow( () -> new IllegalArgumentException("Authentication response body is empty")); @@ -97,11 +81,4 @@ public static Map extractPropertiesFromBody( }); return extractedProperties.isEmpty() ? null : extractedProperties; } - - private static JsonElement getAsJsonElement(final String strResponse) { - return Optional.ofNullable(strResponse) - .filter(response -> !response.isBlank()) - .map(response -> gson.fromJson(response, JsonElement.class)) - .orElse(null); - } } diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/Timeout.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/Timeout.java new file mode 100644 index 0000000000..2f7283ce43 --- /dev/null +++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/Timeout.java @@ -0,0 +1,35 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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 io.camunda.connector.common.utils; + +import com.google.api.client.http.HttpRequest; +import io.camunda.connector.common.model.CommonRequest; +import java.util.concurrent.TimeUnit; + +public class Timeout { + + public static void setTimeout(CommonRequest request, HttpRequest httpRequest) { + if (request.getConnectionTimeoutInSeconds() != null) { + long connectionTimeout = + TimeUnit.SECONDS.toMillis(Long.parseLong(request.getConnectionTimeoutInSeconds())); + int intConnectionTimeout = Math.toIntExact(connectionTimeout); + httpRequest.setConnectTimeout(intConnectionTimeout); + httpRequest.setReadTimeout(intConnectionTimeout); + httpRequest.setWriteTimeout(intConnectionTimeout); + } + } +} diff --git a/connectors/graphql/LICENSE.txt b/connectors/graphql/LICENSE.txt new file mode 100644 index 0000000000..faff62873e --- /dev/null +++ b/connectors/graphql/LICENSE.txt @@ -0,0 +1,5 @@ +Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under one or more contributor license agreements and licensed to you under a proprietary license. +You may not use this file except in compliance with the proprietary license. +The proprietary license can be either the Camunda Platform Self-Managed Free Edition license (available on Camunda’s website) or the Camunda Platform Self-Managed Enterprise Edition license (a copy you obtain when you contact Camunda). +The Camunda Platform Self-Managed Free Edition comes for free but only allows for usage of the software (file) in non-production environments. +If you want to use the software (file) in production, you need to purchase the Camunda Platform Self-Managed Enterprise Edition. diff --git a/connectors/graphql/README.md b/connectors/graphql/README.md new file mode 100644 index 0000000000..c1135043a4 --- /dev/null +++ b/connectors/graphql/README.md @@ -0,0 +1,152 @@ +# Camunda GraphQL Connector + +Find the user documentation in our [Camunda Platform 8 Docs](https://docs.camunda.io/docs/components/integration-framework/connectors/out-of-the-box-connectors/graphql/). + +## Build + +```bash +mvn clean package +``` + +## API + +### Input + +```json +{ + "url": "https://swapi-graphql.netlify.app/.netlify/functions/index", + "method": "post", + "query": "query Root($id: ID) {person (id: $id) {id, name}}", + "variables": "{\"id\": \"cGVvcGxlOjI=\"}" +} +``` + +### Output + +The response will contain the status code, the headers and the body of the response of the GraphQL query response. + +```json +{ + "body": { + "data":{ + "person": { + "id":"cGVvcGxlOjI=", + "name":"C-3PO" + } + } + }, + "headers": { + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "connection": "keep-alive", + "content-length": 56, + "content-type": "application/json; charset=utf-8", + "date": "Tue, 15 Mar 2022 21:31:20 GMT", + "server": "Netlify" + }, + "status": 200 +} +``` + +### Input (Basic) + +```json +{ + "method": "get", + "url": "https://httpbin.org/basic-auth/user/password", + "authentication": { + "type": "basic", + "username": "secrets.USERNAME", + "password": "secrets.PASSWORD" + } +} +``` + +### Output (Bearer Token) + +```json +{ + "method": "get", + "url": "https://httpbin.org/bearer", + "authentication": { + "type": "bearer", + "token": "secrets.TOKEN" + } +} +``` + +### Input (OAuth 2.0) + +```json +{ + "method": "post", + "url": "https://youroauthclientdomainname.eu.auth0.com/oauth/token", + "authentication": { + "oauthTokenEndpoint":"secrets.OAUTH_TOKEN_ENDPOINT_KEY", + "scopes": "read:clients read:users", + "audience":"secrets.AUDIENCE_KEY", + "clientId":"secrets.CLIENT_ID_KEY", + "clientSecret":"secrets.CLIENT_SECRET_KEY", + "type": "oauth-client-credentials-flow", + "clientAuthentication":"secrets.CLIENT_AUTHENTICATION_KEY" + } +} +``` + +### Output (Access Token) + +```json +{ + "access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlUtN2N6WG1sMzljUFNfUnlQQkNMWCJ9.kjhwfjkhfejkrhfbwjkfbhetcetc", + "scope":"read:clients create:users", + "expires_in":86400, + "token_type":"Bearer" +} +``` +### Error codes + +The Connector will fail on any non-2XX HTTP status code in the response. This error status code will be passed on as error code, e.g. "404". + +## Use proxy-mechanism + +The graphQL connector executes the queries/mutations using an HTTP call. You can configure the GraphQL Connector to do any outgoing HTTP call via a proxy. This proxy should be effectively an HTTP JSON Connector +running in a different environment. + +For example, you can build the following runtime architecture: + +``` + Camunda Process --> GraphQL Connector (Proxy-mode) --> HTTP Connector --> GraphQL Endpoint + [ Camunda Network, e.g. K8S ] [ Separate network, e.g. Google Function ] +``` + +Now, any GraphQL query/mutation will be just forwarded to a specified hardcoded URL. And this proxy does the real call then. +This avoids that you could reach internal endpoints in your Camunda network (e.g. the current Kubernetes cluster). + +Just set the following property to enable proxy mode for the connector, e.g. in application.properties when using the Spring-based runtime: + +```properties +camunda.connector.http.proxy.url=https://someUrl/ +``` + +You can also set this via environment variables: + +``` +CAMUNDA_CONNECTOR_HTTP_PROXY_URL=https://someUrl/ +``` + +If the other party requiring OAuth for authentication, you need to set the following environment property: + +```shell +GOOGLE_APPLICATION_CREDENTIALS=... +``` + +### :lock: Test the Connector locally with Google Cloud Function as a proxy + +Run the [:lock:connector-proxy-saas](https://github.com/camunda/connector-proxy-saas) project locally as described in its [:lock:README](https://github.com/camunda/connector-proxy-saas#usage). + +Set the specific property or environment variable to enable proxy mode as described above. + +## Element Template + +The element templates can be found in +the [element-templates/graphql-connector.json](element-templates/graphql-connector.json) file. diff --git a/connectors/graphql/connector.yml b/connectors/graphql/connector.yml new file mode 100644 index 0000000000..f753d96294 --- /dev/null +++ b/connectors/graphql/connector.yml @@ -0,0 +1,3 @@ +name: GRAPHQL +type: io.camunda:connector-graphql:1 +variables: [ graphql, authentication ] \ No newline at end of file diff --git a/connectors/graphql/element-templates/graphql-connector.json b/connectors/graphql/element-templates/graphql-connector.json new file mode 100644 index 0000000000..62c9e73937 --- /dev/null +++ b/connectors/graphql/element-templates/graphql-connector.json @@ -0,0 +1,365 @@ +{ + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "GraphQL Connector", + "id": "io.camunda.connectors.GraphQL.v1", + "description": "Execute GraphQL query", + "version": 1, + "documentationRef": "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/graphql/", + "icon": { + "contents": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' id='GraphQL_Logo' x='0px' y='0px' viewBox='0 0 400 400' enable-background='new 0 0 400 400' xml:space='preserve'%3E%3Cg%3E%3Cg%3E%3Cg%3E%3Crect x='122' y='-0.4' transform='matrix(-0.866 -0.5 0.5 -0.866 163.3196 363.3136)' fill='%23E535AB' width='16.6' height='320.3'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='39.8' y='272.2' fill='%23E535AB' width='320.3' height='16.6'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='37.9' y='312.2' transform='matrix(-0.866 -0.5 0.5 -0.866 83.0693 663.3409)' fill='%23E535AB' width='185' height='16.6'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='177.1' y='71.1' transform='matrix(-0.866 -0.5 0.5 -0.866 463.3409 283.0693)' fill='%23E535AB' width='185' height='16.6'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='122.1' y='-13' transform='matrix(-0.5 -0.866 0.866 -0.5 126.7903 232.1221)' fill='%23E535AB' width='16.6' height='185'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='109.6' y='151.6' transform='matrix(-0.5 -0.866 0.866 -0.5 266.0828 473.3766)' fill='%23E535AB' width='320.3' height='16.6'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='52.5' y='107.5' fill='%23E535AB' width='16.6' height='185'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='330.9' y='107.5' fill='%23E535AB' width='16.6' height='185'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='262.4' y='240.1' transform='matrix(-0.5 -0.866 0.866 -0.5 126.7953 714.2875)' fill='%23E535AB' width='14.5' height='160.9'/%3E%3C/g%3E%3C/g%3E%3Cpath fill='%23E535AB' d='M369.5,297.9c-9.6,16.7-31,22.4-47.7,12.8c-16.7-9.6-22.4-31-12.8-47.7c9.6-16.7,31-22.4,47.7-12.8 C373.5,259.9,379.2,281.2,369.5,297.9'/%3E%3Cpath fill='%23E535AB' d='M90.9,137c-9.6,16.7-31,22.4-47.7,12.8c-16.7-9.6-22.4-31-12.8-47.7c9.6-16.7,31-22.4,47.7-12.8 C94.8,99,100.5,120.3,90.9,137'/%3E%3Cpath fill='%23E535AB' d='M30.5,297.9c-9.6-16.7-3.9-38,12.8-47.7c16.7-9.6,38-3.9,47.7,12.8c9.6,16.7,3.9,38-12.8,47.7 C61.4,320.3,40.1,314.6,30.5,297.9'/%3E%3Cpath fill='%23E535AB' d='M309.1,137c-9.6-16.7-3.9-38,12.8-47.7c16.7-9.6,38-3.9,47.7,12.8c9.6,16.7,3.9,38-12.8,47.7 C340.1,159.4,318.7,153.7,309.1,137'/%3E%3Cpath fill='%23E535AB' d='M200,395.8c-19.3,0-34.9-15.6-34.9-34.9c0-19.3,15.6-34.9,34.9-34.9c19.3,0,34.9,15.6,34.9,34.9 C234.9,380.1,219.3,395.8,200,395.8'/%3E%3Cpath fill='%23E535AB' d='M200,74c-19.3,0-34.9-15.6-34.9-34.9c0-19.3,15.6-34.9,34.9-34.9c19.3,0,34.9,15.6,34.9,34.9 C234.9,58.4,219.3,74,200,74'/%3E%3C/g%3E%3C/svg%3E" + }, + "category": { + "id": "connectors", + "name": "Connectors" + }, + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:ServiceTask" + }, + "groups": [ + { + "id": "authentication", + "label": "Authentication" + }, + { + "id": "endpoint", + "label": "HTTP Endpoint" + }, + { + "id": "graphql", + "label": "GraphQL Query" + }, + { + "id": "timeout", + "label": "Connect Timeout" + }, + { + "id": "output", + "label": "Response Mapping" + }, + { + "id": "errors", + "label": "Error Handling" + } + ], + "properties": [ + { + "type": "Hidden", + "value": "io.camunda:connector-graphql:1", + "binding": { + "type": "zeebe:taskDefinition:type" + } + }, + { + "label": "Type", + "id": "authenticationType", + "group": "authentication", + "description": "Choose the authentication type. Select 'None' if no authentication is necessary", + "value": "noAuth", + "type": "Dropdown", + "choices": [ + { + "name": "None", + "value": "noAuth" + }, + { + "name": "Basic", + "value": "basic" + }, + { + "name": "Bearer Token", + "value": "bearer" + }, + { + "name": "OAuth 2.0", + "value": "oauth-client-credentials-flow" + } + ], + "binding": { + "type": "zeebe:input", + "name": "authentication.type" + } + }, + { + "label": "URL", + "group": "endpoint", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "graphql.url" + }, + "constraints": { + "notEmpty": true, + "pattern": { + "value": "^(=|http://|https://|secrets).*$", + "message": "Must be a http(s) URL." + } + } + }, + { + "id": "method", + "label": "Method", + "group": "endpoint", + "type": "Dropdown", + "value": "post", + "choices": [ + { + "name": "POST", + "value": "post" + }, + { + "name": "GET", + "value": "get" + } + ], + "binding": { + "type": "zeebe:input", + "name": "graphql.method" + } + }, + { + "label": "Query/Mutation", + "description": "See documentation", + "group": "graphql", + "type": "Text", + "language": "graphql", + "binding": { + "type": "zeebe:input", + "name": "graphql.query" + }, + "optional": false, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Variables", + "description": "Learn how to define variables", + "group": "graphql", + "type": "Text", + "feel": "required", + "binding": { + "type": "zeebe:input", + "name": "graphql.variables" + }, + "optional": true + }, + { + "label": "Bearer Token", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "graphql.authentication.token" + }, + "constraints": { + "notEmpty": true + }, + "condition": { + "property": "authenticationType", + "equals": "bearer" + } + }, + { + "label": "OAuth Token Endpoint", + "description": "The OAuth token endpoint", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "authentication.oauthTokenEndpoint" + }, + "constraints": { + "notEmpty": true + }, + "condition": { + "property": "authenticationType", + "equals": "oauth-client-credentials-flow" + } + }, + { + "label": "Client ID", + "description": "Your application's Client ID from the OAuth client", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "authentication.clientId" + }, + "constraints": { + "notEmpty": true + }, + "condition": { + "property": "authenticationType", + "equals": "oauth-client-credentials-flow" + } + }, + { + "label": "Client secret", + "description": "Your application's Client secret from the OAuth client", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "authentication.clientSecret" + }, + "constraints": { + "notEmpty": true + }, + "condition": { + "property": "authenticationType", + "equals": "oauth-client-credentials-flow" + } + }, + { + "label": "Scopes", + "description": "The scopes which you want to request authorization for (e.g.read:contacts)", + "group": "authentication", + "type": "String", + "feel": "optional", + "optional": true, + "binding": { + "type": "zeebe:input", + "name": "authentication.scopes" + }, + "condition": { + "property": "authenticationType", + "equals": "oauth-client-credentials-flow" + } + }, + { + "label": "Audience", + "description": "The unique identifier of the target API you want to access", + "group": "authentication", + "type": "String", + "feel": "optional", + "optional": true, + "binding": { + "type": "zeebe:input", + "name": "authentication.audience" + }, + "condition": { + "property": "authenticationType", + "equals": "oauth-client-credentials-flow" + } + }, + { + "label": "Client Authentication", + "id": "authenticationType", + "group": "authentication", + "description": "Send client id and client secret as Basic Auth request in the header, or as client credentials in the request body", + "value": "basicAuthHeader", + "type": "Dropdown", + "choices": [ + { + "name": "Send client credentials in body", + "value": "credentialsBody" + }, + { + "name": "Send as Basic Auth header", + "value": "basicAuthHeader" + } + ], + "binding": { + "type": "zeebe:input", + "name": "authentication.clientAuthentication" + }, + "condition": { + "property": "authenticationType", + "equals": "oauth-client-credentials-flow" + } + }, + { + "label": "Username", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "authentication.username" + }, + "constraints": { + "notEmpty": true + }, + "condition": { + "property": "authenticationType", + "equals": "basic" + } + }, + { + "label": "Password", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "authentication.password" + }, + "constraints": { + "notEmpty": true + }, + "condition": { + "property": "authenticationType", + "equals": "basic" + } + }, + { + "label": "Connection Timeout", + "description": "Sets the timeout in seconds to establish a connection or 0 for an infinite timeout", + "group": "timeout", + "type": "String", + "value": "20", + "binding": { + "type": "zeebe:input", + "name": "graphql.connectionTimeoutInSeconds" + }, + "optional": true, + "constraints": { + "notEmpty": false, + "pattern": { + "value": "^([0-9]*$)|(secrets.*$)", + "message": "Must be timeout in seconds (default value is 20 seconds)" + } + } + }, + { + "label": "Result Variable", + "description": "Name of variable to store the response in", + "group": "output", + "type": "String", + "binding": { + "type": "zeebe:taskHeader", + "key": "resultVariable" + } + }, + { + "label": "Result Expression", + "description": "Expression to map the response into process variables", + "group": "output", + "type": "Text", + "feel": "required", + "binding": { + "type": "zeebe:taskHeader", + "key": "resultExpression" + } + }, + { + "label": "Error Expression", + "description": "Expression to handle errors. Details in the documentation", + "group": "errors", + "type": "Text", + "feel": "required", + "binding": { + "type": "zeebe:taskHeader", + "key": "errorExpression" + } + } + ] +} \ No newline at end of file diff --git a/connectors/graphql/pom.xml b/connectors/graphql/pom.xml new file mode 100644 index 0000000000..4ca4cc4d8f --- /dev/null +++ b/connectors/graphql/pom.xml @@ -0,0 +1,70 @@ + + 4.0.0 + + + io.camunda.connector + connector-function-parent + 0.16.0-SNAPSHOT + ../pom.xml + + + connector-graphql + graphql connector for Camunda 8 + connector-graphql + jar + + + + Camunda Platform Self-Managed Free Edition license + https://camunda.com/legal/terms/cloud-terms-and-conditions/camunda-cloud-self-managed-free-edition-terms/ + + + Camunda Platform Self-Managed Enterprise Edition license + + + + + + org.danilopianini + gson-extras + + + + com.google.http-client + google-http-client-gson + + + + com.google.http-client + google-http-client-apache-v2 + + + + com.google.auth + google-auth-library-oauth2-http + + + + org.apache.httpcomponents + httpcore + + + + org.slf4j + jcl-over-slf4j + + + + org.apache.commons + commons-text + + + + io.camunda.connector + connectors-common-library + + + + diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/GraphQLFunction.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/GraphQLFunction.java new file mode 100644 index 0000000000..9dc668d808 --- /dev/null +++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/GraphQLFunction.java @@ -0,0 +1,158 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql; + +import static io.camunda.connector.common.utils.Timeout.setTimeout; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.gson.GsonFactory; +import com.google.gson.Gson; +import io.camunda.connector.api.annotation.OutboundConnector; +import io.camunda.connector.api.error.ConnectorException; +import io.camunda.connector.api.outbound.OutboundConnectorContext; +import io.camunda.connector.api.outbound.OutboundConnectorFunction; +import io.camunda.connector.common.auth.OAuthAuthentication; +import io.camunda.connector.common.constants.Constants; +import io.camunda.connector.common.model.CommonRequest; +import io.camunda.connector.common.model.CommonResult; +import io.camunda.connector.common.services.AuthenticationService; +import io.camunda.connector.common.services.HTTPProxyService; +import io.camunda.connector.common.services.HTTPService; +import io.camunda.connector.graphql.components.GsonComponentSupplier; +import io.camunda.connector.graphql.components.HttpTransportComponentSupplier; +import io.camunda.connector.graphql.model.GraphQLRequest; +import io.camunda.connector.graphql.model.GraphQLResult; +import io.camunda.connector.graphql.utils.GraphQLRequestMapper; +import io.camunda.connector.graphql.utils.JsonSerializeHelper; +import io.camunda.connector.impl.config.ConnectorConfigurationUtil; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@OutboundConnector( + name = "GRAPHQL", + inputVariables = {"graphql", "authentication"}, + type = "io.camunda:connector-graphql:1") +public class GraphQLFunction implements OutboundConnectorFunction { + + private static final Logger LOGGER = LoggerFactory.getLogger(GraphQLFunction.class); + + private final Gson gson; + private final GsonFactory gsonFactory; + private final HttpRequestFactory requestFactory; + + private final String proxyFunctionUrl; + + public GraphQLFunction() { + this(ConnectorConfigurationUtil.getProperty(Constants.PROXY_FUNCTION_URL_ENV_NAME)); + } + + public GraphQLFunction(String proxyFunctionUrl) { + this( + GsonComponentSupplier.gsonInstance(), + HttpTransportComponentSupplier.httpRequestFactoryInstance(), + GsonComponentSupplier.gsonFactoryInstance(), + proxyFunctionUrl); + } + + public GraphQLFunction( + final Gson gson, + final HttpRequestFactory requestFactory, + final GsonFactory gsonFactory, + final String proxyFunctionUrl) { + this.gson = gson; + this.requestFactory = requestFactory; + this.gsonFactory = gsonFactory; + this.proxyFunctionUrl = proxyFunctionUrl; + } + + @Override + public Object execute(OutboundConnectorContext context) + throws IOException, InstantiationException, IllegalAccessException { + final var json = context.getVariables(); + var connectorRequest = JsonSerializeHelper.serializeRequest(gson, json); + context.validate(connectorRequest); + context.replaceSecrets(connectorRequest); + + return StringUtils.isBlank(proxyFunctionUrl) + ? executeGraphQLConnector(connectorRequest) + : executeGraphQLConnectorViaProxy(connectorRequest); + } + + private GraphQLResult executeGraphQLConnector(final GraphQLRequest connectorRequest) + throws IOException, InstantiationException, IllegalAccessException { + // connector logic + LOGGER.debug("Executing graphql connector with request {}", connectorRequest); + HTTPService httpService = new HTTPService(gson); + AuthenticationService authService = new AuthenticationService(gson, requestFactory); + String bearerToken = null; + if (connectorRequest.getAuthentication() != null + && connectorRequest.getAuthentication() instanceof OAuthAuthentication) { + final HttpRequest oauthRequest = authService.createOAuthRequest(connectorRequest); + final HttpResponse oauthResponse = httpService.executeHttpRequest(oauthRequest); + bearerToken = authService.extractOAuthAccessToken(oauthResponse); + } + + final HttpRequest httpRequest = createRequest(httpService, connectorRequest, bearerToken); + HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest); + return httpService.toHttpJsonResponse(httpResponse, GraphQLResult.class); + } + + private CommonResult executeGraphQLConnectorViaProxy(GraphQLRequest request) throws IOException { + CommonRequest commonRequest = GraphQLRequestMapper.toCommonRequest(request); + HttpRequest httpRequest = + HTTPProxyService.toRequestViaProxy(gson, requestFactory, commonRequest, proxyFunctionUrl); + + HTTPService httpService = new HTTPService(gson); + + HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest, true); + + try (InputStream responseContentStream = httpResponse.getContent(); + Reader reader = new InputStreamReader(responseContentStream)) { + final CommonResult jsonResult = gson.fromJson(reader, CommonResult.class); + LOGGER.debug("Proxy returned result: " + jsonResult); + return jsonResult; + } catch (final Exception e) { + LOGGER.debug("Failed to parse external response: {}", httpResponse, e); + throw new ConnectorException("Failed to parse result: " + e.getMessage(), e); + } + } + + public HttpRequest createRequest( + final HTTPService httpService, final GraphQLRequest request, String bearerToken) + throws IOException { + final String method = request.getMethod().toUpperCase(); + final GenericUrl genericUrl = new GenericUrl(request.getUrl()); + HttpContent content = null; + final HttpHeaders headers = httpService.createHeaders(request, bearerToken); + final Map queryAndVariablesMap = + JsonSerializeHelper.queryAndVariablesToMap(request); + if (Constants.POST.equalsIgnoreCase(method)) { + content = new JsonHttpContent(gsonFactory, queryAndVariablesMap); + } else { + genericUrl.putAll(queryAndVariablesMap); + } + + final var httpRequest = requestFactory.buildRequest(method, genericUrl, content); + httpRequest.setFollowRedirects(false); + setTimeout(request, httpRequest); + httpRequest.setHeaders(headers); + + return httpRequest; + } +} diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/GsonComponentSupplier.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/GsonComponentSupplier.java new file mode 100644 index 0000000000..8cbdfdaf50 --- /dev/null +++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/GsonComponentSupplier.java @@ -0,0 +1,44 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql.components; + +import com.google.api.client.json.gson.GsonFactory; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; +import com.google.gson.typeadapters.RuntimeTypeAdapterFactory; +import io.camunda.connector.common.auth.Authentication; +import io.camunda.connector.common.auth.BasicAuthentication; +import io.camunda.connector.common.auth.BearerAuthentication; +import io.camunda.connector.common.auth.NoAuthentication; +import io.camunda.connector.common.auth.OAuthAuthentication; + +public class GsonComponentSupplier { + + private static final GsonFactory GSON_FACTORY = new GsonFactory(); + private static final Gson GSON = + new GsonBuilder() + .serializeNulls() + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .registerTypeAdapterFactory( + RuntimeTypeAdapterFactory.of(Authentication.class, "type") + .registerSubtype(NoAuthentication.class, "noAuth") + .registerSubtype(BasicAuthentication.class, "basic") + .registerSubtype(BearerAuthentication.class, "bearer") + .registerSubtype(OAuthAuthentication.class, "oauth-client-credentials-flow")) + .create(); + + private GsonComponentSupplier() {} + + public static Gson gsonInstance() { + return GSON; + } + + public static GsonFactory gsonFactoryInstance() { + return GSON_FACTORY; + } +} diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/HttpTransportComponentSupplier.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/HttpTransportComponentSupplier.java new file mode 100644 index 0000000000..df07e6a26d --- /dev/null +++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/HttpTransportComponentSupplier.java @@ -0,0 +1,27 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql.components; + +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.api.client.json.JsonObjectParser; + +public class HttpTransportComponentSupplier { + + private HttpTransportComponentSupplier() {} + + private static final HttpTransport HTTP_TRANSPORT = new ApacheHttpTransport(); + private static final HttpRequestFactory REQUEST_FACTORY = + HTTP_TRANSPORT.createRequestFactory( + request -> + request.setParser(new JsonObjectParser(GsonComponentSupplier.gsonFactoryInstance()))); + + public static HttpRequestFactory httpRequestFactoryInstance() { + return REQUEST_FACTORY; + } +} diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequest.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequest.java new file mode 100644 index 0000000000..e4ad7e1dc9 --- /dev/null +++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql.model; + +import io.camunda.connector.api.annotation.Secret; +import io.camunda.connector.common.model.CommonRequest; +import java.util.Objects; +import javax.validation.constraints.NotBlank; + +public class GraphQLRequest extends CommonRequest { + + @NotBlank @Secret private String query; + @Secret private Object variables; + + public boolean hasQuery() { + return query != null; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Object getVariables() { + return variables; + } + + public void setVariables(Object variables) { + this.variables = variables; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GraphQLRequest that = (GraphQLRequest) o; + return query.equals(that.query) && Objects.equals(variables, that.variables); + } + + @Override + public int hashCode() { + return Objects.hash(query, variables); + } + + @Override + public String toString() { + return "GraphQLRequest{" + "query='" + query + '\'' + ", variables=" + variables + '}'; + } +} diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequestWrapper.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequestWrapper.java new file mode 100644 index 0000000000..05d447ce02 --- /dev/null +++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequestWrapper.java @@ -0,0 +1,55 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql.model; + +import io.camunda.connector.common.auth.Authentication; +import java.util.Objects; +import javax.validation.constraints.NotBlank; + +public class GraphQLRequestWrapper { + @NotBlank private GraphQLRequest graphql; + private Authentication authentication; + + public GraphQLRequest getGraphql() { + return graphql; + } + + public void setGraphql(GraphQLRequest graphql) { + this.graphql = graphql; + } + + public Authentication getAuthentication() { + return authentication; + } + + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GraphQLRequestWrapper that = (GraphQLRequestWrapper) o; + return graphql.equals(that.graphql) && Objects.equals(authentication, that.authentication); + } + + @Override + public int hashCode() { + return Objects.hash(graphql, authentication); + } + + @Override + public String toString() { + return "GraphQLRequestWrapper{" + + "graphql=" + + graphql + + ", authentication=" + + authentication + + '}'; + } +} diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLResult.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLResult.java new file mode 100644 index 0000000000..0d766d9646 --- /dev/null +++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLResult.java @@ -0,0 +1,11 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql.model; + +import io.camunda.connector.common.model.CommonResult; + +public class GraphQLResult extends CommonResult {} diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/GraphQLRequestMapper.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/GraphQLRequestMapper.java new file mode 100644 index 0000000000..65f167b20f --- /dev/null +++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/GraphQLRequestMapper.java @@ -0,0 +1,35 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql.utils; + +import io.camunda.connector.common.constants.Constants; +import io.camunda.connector.common.model.CommonRequest; +import io.camunda.connector.graphql.model.GraphQLRequest; +import java.util.Map; +import java.util.stream.Collectors; + +public final class GraphQLRequestMapper { + + public static CommonRequest toCommonRequest(GraphQLRequest graphQLRequest) { + CommonRequest commonRequest = new CommonRequest(); + final Map queryAndVariablesMap = + JsonSerializeHelper.queryAndVariablesToMap(graphQLRequest); + if (Constants.POST.equalsIgnoreCase(graphQLRequest.getMethod())) { + commonRequest.setBody(queryAndVariablesMap); + } else { + final Map queryAndVariablesStringMap = + queryAndVariablesMap.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> String.valueOf(e.getValue()))); + commonRequest.setQueryParameters(queryAndVariablesStringMap); + } + commonRequest.setAuthentication(graphQLRequest.getAuthentication()); + commonRequest.setMethod(graphQLRequest.getMethod()); + commonRequest.setUrl(graphQLRequest.getUrl()); + commonRequest.setConnectionTimeoutInSeconds(graphQLRequest.getConnectionTimeoutInSeconds()); + return commonRequest; + } +} diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/JsonSerializeHelper.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/JsonSerializeHelper.java new file mode 100644 index 0000000000..85877ace4b --- /dev/null +++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/JsonSerializeHelper.java @@ -0,0 +1,35 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql.utils; + +import com.google.gson.Gson; +import io.camunda.connector.graphql.model.GraphQLRequest; +import io.camunda.connector.graphql.model.GraphQLRequestWrapper; +import java.util.HashMap; +import java.util.Map; + +public final class JsonSerializeHelper { + public static GraphQLRequest serializeRequest(Gson gson, String input) { + GraphQLRequestWrapper graphQLRequestWrapper = gson.fromJson(input, GraphQLRequestWrapper.class); + GraphQLRequest graphQLRequest = graphQLRequestWrapper.getGraphql(); + graphQLRequest.setAuthentication(graphQLRequestWrapper.getAuthentication()); + return graphQLRequest; + } + + public static Map queryAndVariablesToMap(GraphQLRequest graphQLRequest) { + final Map map = new HashMap<>(); + map.put("query", getEscapedQuery(graphQLRequest)); + if (graphQLRequest.getVariables() != null) { + map.put("variables", graphQLRequest.getVariables()); + } + return map; + } + + public static String getEscapedQuery(GraphQLRequest graphQLRequest) { + return graphQLRequest.getQuery().replace("\\n", "").replace("\\\"", "\""); + } +} diff --git a/connectors/graphql/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction b/connectors/graphql/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction new file mode 100644 index 0000000000..3321632136 --- /dev/null +++ b/connectors/graphql/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction @@ -0,0 +1 @@ +io.camunda.connector.graphql.GraphQLFunction \ No newline at end of file diff --git a/connectors/graphql/src/test/java/io/camunda/connector/graphql/BaseTest.java b/connectors/graphql/src/test/java/io/camunda/connector/graphql/BaseTest.java new file mode 100644 index 0000000000..34a391e999 --- /dev/null +++ b/connectors/graphql/src/test/java/io/camunda/connector/graphql/BaseTest.java @@ -0,0 +1,132 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.readString; + +import com.google.api.client.json.gson.GsonFactory; +import com.google.gson.Gson; +import io.camunda.connector.graphql.components.GsonComponentSupplier; +import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; + +public class BaseTest { + + protected Gson gson = GsonComponentSupplier.gsonInstance(); + + protected GsonFactory gsonFactory = GsonComponentSupplier.gsonFactoryInstance(); + + protected interface SecretsConstant { + String URL = "URL_KEY"; + String METHOD = "METHOD_KEY"; + String CONNECT_TIMEOUT = "CONNECT_TIMEOUT_KEY"; + + interface Authentication { + String TOKEN = "TOKEN_KEY"; + String PASSWORD = "PASSWORD_KEY"; + String USERNAME = "USERNAME_KEY"; + String OAUTH_TOKEN_ENDPOINT = "OAUTH_TOKEN_ENDPOINT_KEY"; + String CLIENT_ID = "CLIENT_ID_KEY"; + String CLIENT_SECRET = "CLIENT_SECRET_KEY"; + String AUDIENCE = "AUDIENCE_KEY"; + } + + interface Variables { + String ID = "VARIABLE_ID"; + } + + interface Query { + String ID = "QUERY_ID"; + String TEXT = "TEXT_KEY"; + String TEXT_PART_1 = "TEXT_PART_1_KEY"; + String TEXT_PART_2 = "TEXT_PART_2_KEY"; + String TEXT_PART_3 = "TEXT_PART_3_KEY"; + } + } + + protected interface ActualValue { + String URL = "https://camunda.io/http-endpoint"; + String METHOD = "GET"; + String CONNECT_TIMEOUT = "50"; + + interface Authentication { + String TOKEN = "test token"; + String PASSWORD = "1234567890"; + String USERNAME = "test username"; + String OAUTH_TOKEN_ENDPOINT = "https://test/api/v2/"; + String CLIENT_ID = "bi1cekB123456GRWBBEgzdxA89S2T"; + String CLIENT_SECRET = "Bzw6SL12345678934562eqg4fJM72EeeM2JQiF4BfbyYZUDCur7ntB"; + String AUDIENCE = "https://test/api/v2/"; + } + + interface Variables { + String ID = "variableId"; + String USER_AGENT = "http-connector-demo"; + } + + interface Query { + String ID = "secret id key"; + String CUSTOMER_NAME_SECRET = "secret name"; + String CUSTOMER_NAME_REAL = CUSTOMER_NAME_SECRET + " plus some text"; + String CUSTOMER_EMAIL_SECRET = "start email plus secret email part plus end email"; + String CUSTOMER_EMAIL_REAL = "start email plus " + CUSTOMER_EMAIL_SECRET + " plus end email"; + String TEXT_PART_1 = "start secret text plus "; + String TEXT_PART_2 = "mid of text plus "; + String TEXT_PART_3 = "end of text"; + String TEXT = TEXT_PART_1 + TEXT_PART_2 + TEXT_PART_3; + } + } + + protected interface JsonKeys { + String CLUSTER_ID = "X-Camunda-Cluster-ID"; + String USER_AGENT = "User-Agent"; + String QUERY = "q"; + String PRIORITY = "priority"; + String CUSTOMER = "customer"; + String ID = "id"; + String NAME = "name"; + String EMAIL = "email"; + String TEXT = "text"; + } + + protected OutboundConnectorContextBuilder getContextBuilderWithSecrets() { + return OutboundConnectorContextBuilder.create() + .secret(SecretsConstant.URL, ActualValue.URL) + .secret(SecretsConstant.METHOD, ActualValue.METHOD) + .secret(SecretsConstant.CONNECT_TIMEOUT, ActualValue.CONNECT_TIMEOUT) + .secret(SecretsConstant.Authentication.TOKEN, ActualValue.Authentication.TOKEN) + .secret(SecretsConstant.Authentication.USERNAME, ActualValue.Authentication.USERNAME) + .secret(SecretsConstant.Authentication.PASSWORD, ActualValue.Authentication.PASSWORD) + .secret( + SecretsConstant.Authentication.OAUTH_TOKEN_ENDPOINT, + ActualValue.Authentication.OAUTH_TOKEN_ENDPOINT) + .secret(SecretsConstant.Authentication.CLIENT_ID, ActualValue.Authentication.CLIENT_ID) + .secret( + SecretsConstant.Authentication.CLIENT_SECRET, ActualValue.Authentication.CLIENT_SECRET) + .secret(SecretsConstant.Authentication.AUDIENCE, ActualValue.Authentication.AUDIENCE) + .secret(SecretsConstant.Variables.ID, ActualValue.Variables.ID) + .secret(SecretsConstant.Query.ID, ActualValue.Query.ID) + .secret(SecretsConstant.Query.TEXT, ActualValue.Query.TEXT) + .secret(SecretsConstant.Query.TEXT_PART_1, ActualValue.Query.TEXT_PART_1) + .secret(SecretsConstant.Query.TEXT_PART_2, ActualValue.Query.TEXT_PART_2) + .secret(SecretsConstant.Query.TEXT_PART_3, ActualValue.Query.TEXT_PART_3); + } + + @SuppressWarnings("unchecked") + protected static Stream loadTestCasesFromResourceFile(final String fileWithTestCasesUri) + throws IOException { + final String cases = readString(new File(fileWithTestCasesUri).toPath(), UTF_8); + final Gson testingGson = GsonComponentSupplier.gsonInstance(); + var array = testingGson.fromJson(cases, ArrayList.class); + return array.stream().map(testingGson::toJson).map(Arguments::of); + } +} diff --git a/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionInputValidationTest.java b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionInputValidationTest.java new file mode 100644 index 0000000000..99c35c80ea --- /dev/null +++ b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionInputValidationTest.java @@ -0,0 +1,144 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.camunda.connector.api.outbound.OutboundConnectorContext; +import io.camunda.connector.graphql.model.GraphQLRequest; +import io.camunda.connector.graphql.utils.JsonSerializeHelper; +import io.camunda.connector.impl.ConnectorInputException; +import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder; +import java.io.IOException; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class GraphQLFunctionInputValidationTest extends BaseTest { + + private static final String FAIL_REQUEST_CASES_PATH = + "src/test/resources/requests/fail-cases-request-without-one-required-field.json"; + + private static final String FAIL_CASES_TIMEOUT_CONNECTION_RESOURCE_PATH = + "src/test/resources/requests/fail-cases-connection-timeout-validation.json"; + + private static final String SUCCESS_CASES_TIMEOUT_CONNECTION_RESOURCE_PATH = + "src/test/resources/requests/success-cases-connection-timeout-validation.json"; + + private static final String REQUEST_METHOD_OBJECT_PLACEHOLDER = + "{\"graphql\":{\n \"method\": \"%s\",\n \"url\": \"https://camunda.io/http-endpoint\"\n}}"; + + private static final String REQUEST_ENDPOINT_OBJECT_PLACEHOLDER = + "{\"graphql\":{\n \"method\": \"get\",\n \"url\": \"%s\"\n}}"; + + private GraphQLFunction functionUnderTest; + + @BeforeEach + void setup() { + functionUnderTest = new GraphQLFunction(null); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\r\n"}) + void shouldRaiseException_WhenExecuted_MethodMalformed(final String input) { + // Given + OutboundConnectorContext ctx = + OutboundConnectorContextBuilder.create() + .variables(String.format(REQUEST_METHOD_OBJECT_PLACEHOLDER, input)) + .build(); + + // When + Throwable exception = + assertThrows(ConnectorInputException.class, () -> functionUnderTest.execute(ctx)); + + // Then + assertThat(exception.getMessage()) + .contains("Found constraints violated while validating input", "method: must not be blank"); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "iAmWrongUrl", "ftp://camunda.org/", "camunda@camunda.com"}) + void shouldRaiseException_WhenExecuted_EndpointMalformed(final String input) { + // Given + OutboundConnectorContext ctx = + OutboundConnectorContextBuilder.create() + .variables(String.format(REQUEST_ENDPOINT_OBJECT_PLACEHOLDER, input)) + .build(); + // When + Throwable exception = + assertThrows(ConnectorInputException.class, () -> functionUnderTest.execute(ctx)); + // Then + assertThat(exception.getMessage()) + .contains( + "Found constraints violated while validating input", + "must match \"^(http://|https://|secrets).*$\""); + } + + @ParameterizedTest(name = "Validate null field # {index}") + @MethodSource("failRequestCases") + void validate_shouldThrowExceptionWhenLeastOneNotExistRequestField(String input) { + // Given request without one required field + GraphQLRequest httpJsonRequest = JsonSerializeHelper.serializeRequest(gson, input); + OutboundConnectorContext context = + OutboundConnectorContextBuilder.create().variables(httpJsonRequest).build(); + // When context.validate(request); + // Then expect exception that one required field not set + ConnectorInputException thrown = + assertThrows( + ConnectorInputException.class, + () -> context.validate(httpJsonRequest), + "ConnectorInputException was expected"); + assertThat(thrown.getMessage()).contains("Found constraints violated while validating input"); + } + + @ParameterizedTest(name = "Validate connectionTimeout # {index}") + @MethodSource("failTimeOutConnectionCases") + void validate_shouldThrowExceptionConnectionTimeoutIsWrong(String input) { + // Given request without one required field + GraphQLRequest httpJsonRequest = JsonSerializeHelper.serializeRequest(gson, input); + OutboundConnectorContext context = + OutboundConnectorContextBuilder.create().variables(httpJsonRequest).build(); + // When context.validate(request); + // Then expect exception + ConnectorInputException thrown = + assertThrows( + ConnectorInputException.class, + () -> context.validate(httpJsonRequest), + "ConnectorInputException was expected"); + assertThat(thrown.getMessage()).contains("Found constraints violated while validating input"); + } + + @ParameterizedTest(name = "Success validate connectionTimeout # {index}") + @MethodSource("successTimeOutConnectionCases") + void validate_shouldValidateWithoutException(String input) { + // Given request without one required field + GraphQLRequest httpJsonRequest = JsonSerializeHelper.serializeRequest(gson, input); + OutboundConnectorContext context = + OutboundConnectorContextBuilder.create().variables(httpJsonRequest).build(); + // When context.validate(request); + // Then expect normal validate without exception + context.validate(httpJsonRequest); + } + + protected static Stream failRequestCases() throws IOException { + return loadTestCasesFromResourceFile(FAIL_REQUEST_CASES_PATH); + } + + private static Stream failTimeOutConnectionCases() throws IOException { + return loadTestCasesFromResourceFile(FAIL_CASES_TIMEOUT_CONNECTION_RESOURCE_PATH); + } + + private static Stream successTimeOutConnectionCases() throws IOException { + return loadTestCasesFromResourceFile(SUCCESS_CASES_TIMEOUT_CONNECTION_RESOURCE_PATH); + } +} diff --git a/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionSecretsTest.java b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionSecretsTest.java new file mode 100644 index 0000000000..b0f62f31e2 --- /dev/null +++ b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionSecretsTest.java @@ -0,0 +1,135 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.JsonObject; +import io.camunda.connector.api.outbound.OutboundConnectorContext; +import io.camunda.connector.common.auth.Authentication; +import io.camunda.connector.common.auth.BasicAuthentication; +import io.camunda.connector.common.auth.BearerAuthentication; +import io.camunda.connector.common.auth.NoAuthentication; +import io.camunda.connector.common.auth.OAuthAuthentication; +import io.camunda.connector.graphql.model.GraphQLRequest; +import io.camunda.connector.graphql.utils.JsonSerializeHelper; +import java.io.IOException; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class GraphQLFunctionSecretsTest extends BaseTest { + private static final String SUCCESS_REPLACE_SECRETS_CASES_PATH = + "src/test/resources/requests/success-cases-replace-secrets.json"; + + private OutboundConnectorContext context; + + protected static Stream successReplaceSecretsCases() throws IOException { + return loadTestCasesFromResourceFile(SUCCESS_REPLACE_SECRETS_CASES_PATH); + } + + @ParameterizedTest(name = "Should replace request secrets") + @MethodSource("successReplaceSecretsCases") + void replaceSecrets_shouldReplaceRequestSecrets(String input) { + // Given request with secrets + GraphQLRequest graphQLRequest = JsonSerializeHelper.serializeRequest(gson, input); + context = getContextBuilderWithSecrets().variables(graphQLRequest).build(); + // When + context.replaceSecrets(graphQLRequest); + // Then should replace secrets + assertThat(graphQLRequest.getUrl()).isEqualTo(ActualValue.URL); + assertThat(graphQLRequest.getMethod()).isEqualTo(ActualValue.METHOD); + assertThat(graphQLRequest.getConnectionTimeoutInSeconds()) + .isEqualTo(ActualValue.CONNECT_TIMEOUT); + } + + @ParameterizedTest(name = "Should replace auth secrets") + @MethodSource("successReplaceSecretsCases") + void replaceSecrets_shouldReplaceAuthSecrets(String input) { + // Given request with secrets + GraphQLRequest graphQLRequest = JsonSerializeHelper.serializeRequest(gson, input); + context = getContextBuilderWithSecrets().variables(graphQLRequest).build(); + // When + context.replaceSecrets(graphQLRequest); + // Then should replace secrets + Authentication authentication = graphQLRequest.getAuthentication(); + if (authentication instanceof NoAuthentication) { + // nothing check in this case + } else if (authentication instanceof BearerAuthentication) { + BearerAuthentication bearerAuth = (BearerAuthentication) authentication; + assertThat(bearerAuth.getToken()).isEqualTo(ActualValue.Authentication.TOKEN); + } else if (authentication instanceof BasicAuthentication) { + BasicAuthentication basicAuth = (BasicAuthentication) authentication; + assertThat(basicAuth.getPassword()).isEqualTo(ActualValue.Authentication.PASSWORD); + assertThat(basicAuth.getUsername()).isEqualTo(ActualValue.Authentication.USERNAME); + } else if (authentication instanceof OAuthAuthentication) { + OAuthAuthentication oAuthAuthentication = (OAuthAuthentication) authentication; + assertThat(oAuthAuthentication.getOauthTokenEndpoint()) + .isEqualTo(ActualValue.Authentication.OAUTH_TOKEN_ENDPOINT); + assertThat(oAuthAuthentication.getClientId()).isEqualTo(ActualValue.Authentication.CLIENT_ID); + assertThat(oAuthAuthentication.getClientSecret()) + .isEqualTo(ActualValue.Authentication.CLIENT_SECRET); + assertThat(oAuthAuthentication.getAudience()).isEqualTo(ActualValue.Authentication.AUDIENCE); + } else { + fail("unknown authentication type"); + } + } + + @ParameterizedTest(name = "Should replace variables secrets") + @MethodSource("successReplaceSecretsCases") + void replaceSecrets_shouldReplaceVariablesSecrets(String input) { + // Given request with secrets + // GraphQLRequestWrapper graphQLRequest = gson.fromJson(input, GraphQLRequestWrapper.class); + GraphQLRequest graphQLRequest = JsonSerializeHelper.serializeRequest(gson, input); + context = getContextBuilderWithSecrets().variables(graphQLRequest).build(); + // When + context.replaceSecrets(graphQLRequest); + // Then should replace secrets + JsonObject variables = gson.toJsonTree(graphQLRequest.getVariables()).getAsJsonObject(); + + assertThat(variables.get(JsonKeys.ID).getAsString()).isEqualTo(ActualValue.Variables.ID); + } + + @ParameterizedTest(name = "Should replace query secrets") + @MethodSource("successReplaceSecretsCases") + void replaceSecrets_shouldReplaceQuerySecrets(String input) { + // Given request with secrets + GraphQLRequest graphQLRequest = JsonSerializeHelper.serializeRequest(gson, input); + context = getContextBuilderWithSecrets().variables(graphQLRequest).build(); + // When + context.replaceSecrets(graphQLRequest); + // Then should replace secrets + String query = graphQLRequest.getQuery(); + assertFalse(query.contains("{{secrets.QUERY_ID}}")); + assertTrue(query.contains(ActualValue.Query.ID)); + } + + @Test + void replaceSecrets_shouldReplaceQueryWhenQueryIsString() { + // Given request with secrets + GraphQLRequest request = new GraphQLRequest(); + request.setQuery( + "{{secrets." + + SecretsConstant.Query.TEXT_PART_1 + + "}}" + + "{{secrets." + + SecretsConstant.Query.TEXT_PART_2 + + "}}" + + "{{secrets." + + SecretsConstant.Query.TEXT_PART_3 + + "}}"); + context = getContextBuilderWithSecrets().variables(request).build(); + // When + context.replaceSecrets(request); + // Then should replace secrets + assertThat(request.getQuery().toString()).isEqualTo(ActualValue.Query.TEXT); + } +} diff --git a/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionTest.java b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionTest.java new file mode 100644 index 0000000000..babc6f3da6 --- /dev/null +++ b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionTest.java @@ -0,0 +1,212 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.graphql; + +import static org.apache.http.entity.ContentType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.gson.JsonObject; +import io.camunda.connector.api.error.ConnectorException; +import io.camunda.connector.graphql.model.GraphQLRequest; +import io.camunda.connector.graphql.model.GraphQLResult; +import io.camunda.connector.impl.ConnectorInputException; +import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder; +import java.io.IOException; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class GraphQLFunctionTest extends BaseTest { + + private static final String SUCCESS_CASES_RESOURCE_PATH = + "src/test/resources/requests/success-test-cases.json"; + + private static final String SUCCESS_CASES_OAUTH_RESOURCE_PATH = + "src/test/resources/requests/success-test-cases-oauth.json"; + private static final String FAIL_CASES_RESOURCE_PATH = + "src/test/resources/requests/fail-test-cases.json"; + + @Mock private HttpRequestFactory requestFactory; + @Mock private HttpRequest httpRequest; + @Mock private HttpResponse httpResponse; + + private GraphQLFunction functionUnderTest; + + @BeforeEach + public void setup() { + functionUnderTest = new GraphQLFunction(gson, requestFactory, gsonFactory, null); + } + + @ParameterizedTest(name = "Executing test case: {0}") + @MethodSource("successCases") + void shouldReturnResult_WhenExecuted(final String input) + throws IOException, InstantiationException, IllegalAccessException { + // given - minimal required entity + Object functionCallResponseAsObject = arrange(input); + + // then + verify(httpRequest).execute(); + assertThat(functionCallResponseAsObject).isInstanceOf(GraphQLResult.class); + assertThat(((GraphQLResult) functionCallResponseAsObject).getHeaders()) + .containsValue(APPLICATION_JSON.getMimeType()); + } + + @ParameterizedTest(name = "Executing test case: {0}") + @MethodSource("successCasesOauth") + void shouldReturnResultOAuth_WhenExecuted(final String input) + throws IOException, InstantiationException, IllegalAccessException { + Object functionCallResponseAsObject = arrange(input); + + // then + verify(httpRequest, times(2)).execute(); + assertThat(functionCallResponseAsObject).isInstanceOf(GraphQLResult.class); + assertThat(((GraphQLResult) functionCallResponseAsObject).getHeaders()) + .containsValue(APPLICATION_JSON.getMimeType()); + } + + private Object arrange(String input) + throws IOException, InstantiationException, IllegalAccessException { + final var context = + OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build(); + when(requestFactory.buildRequest( + anyString(), any(GenericUrl.class), nullable(HttpContent.class))) + .thenReturn(httpRequest); + when(httpResponse.getHeaders()) + .thenReturn(new HttpHeaders().setContentType(APPLICATION_JSON.getMimeType())); + when(httpRequest.execute()).thenReturn(httpResponse); + + // when + return functionUnderTest.execute(context); + } + + @ParameterizedTest(name = "Executing test case: {0}") + @MethodSource("failCases") + void shouldReturnFallbackResult_WhenMalformedRequest(final String input) { + final var context = + OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build(); + + // when + var exceptionThrown = catchException(() -> functionUnderTest.execute(context)); + + // then + assertThat(exceptionThrown) + .isInstanceOf(ConnectorInputException.class) + .hasMessageContaining("ValidationException"); + } + + @ParameterizedTest(name = "Executing test case: {0}") + @MethodSource("successCases") + void execute_shouldSetConnectTime(final String input) + throws IOException, InstantiationException, IllegalAccessException { + // given - minimal required entity + final var context = + OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build(); + final var expectedTimeInMilliseconds = + Integer.parseInt( + gson.fromJson( + gson.fromJson(input, JsonObject.class).get("graphql").toString(), + GraphQLRequest.class) + .getConnectionTimeoutInSeconds()) + * 1000; + + when(requestFactory.buildRequest( + anyString(), any(GenericUrl.class), nullable(HttpContent.class))) + .thenReturn(httpRequest); + when(httpResponse.getHeaders()) + .thenReturn(new HttpHeaders().setContentType(APPLICATION_JSON.getMimeType())); + when(httpRequest.execute()).thenReturn(httpResponse); + // when + functionUnderTest.execute(context); + // then + verify(httpRequest).setConnectTimeout(expectedTimeInMilliseconds); + } + + @ParameterizedTest + @ValueSource(ints = {400, 404, 500}) + void execute_shouldPassOnHttpErrorAsErrorCode(final int input) throws IOException { + // given + final var request = + "{\"graphql\": { \"method\": \"get\", \"url\": \"https://camunda.io/http-endpoint\", \"query\": \"testQuery\"}, \"authentication\": { \"type\": \"noAuth\" } }"; + final var context = OutboundConnectorContextBuilder.create().variables(request).build(); + + when(requestFactory.buildRequest( + anyString(), any(GenericUrl.class), nullable(HttpContent.class))) + .thenReturn(httpRequest); + when(httpResponse.getHeaders()) + .thenReturn(new HttpHeaders().setContentType(APPLICATION_JSON.getMimeType())); + when(httpResponse.getStatusCode()).thenReturn(input); + when(httpResponse.parseAsString()).thenReturn("message"); + doThrow(new HttpResponseException(httpResponse)).when(httpRequest).execute(); + // when + final var result = catchException(() -> functionUnderTest.execute(context)); + // then HTTP status code is passed on as error code + assertThat(result) + .isInstanceOf(ConnectorException.class) + .extracting("errorCode") + .isEqualTo(String.valueOf(input)); + } + + @Test + void execute_shouldNotUseErrorDataOnHttpError() throws IOException { + // given + final var request = + "{\"graphql\": {\"method\": \"get\", \"url\": \"https://camunda.io/http-endpoint\", \"query\": \"testQuery\"}, \"authentication\": { \"type\": \"noAuth\" } }"; + final var context = OutboundConnectorContextBuilder.create().variables(request).build(); + final var httpException = mock(HttpResponseException.class); + + when(requestFactory.buildRequest( + anyString(), any(GenericUrl.class), nullable(HttpContent.class))) + .thenReturn(httpRequest); + when(httpException.getStatusCode()).thenReturn(500); + when(httpException.getMessage()).thenReturn("message"); + doThrow(httpException).when(httpRequest).execute(); + // when + final var result = catchException(() -> functionUnderTest.execute(context)); + // then HTTP status code is passed on as error code + verify(httpException, times(0)).getContent(); + assertThat(result) + .isInstanceOf(ConnectorException.class) + .hasMessage("message") + .extracting("errorCode") + .isEqualTo("500"); + } + + private static Stream successCases() throws IOException { + return loadTestCasesFromResourceFile(SUCCESS_CASES_RESOURCE_PATH); + } + + private static Stream successCasesOauth() throws IOException { + return loadTestCasesFromResourceFile(SUCCESS_CASES_OAUTH_RESOURCE_PATH); + } + + private static Stream failCases() throws IOException { + return loadTestCasesFromResourceFile(FAIL_CASES_RESOURCE_PATH); + } +} diff --git a/connectors/graphql/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/connectors/graphql/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/connectors/graphql/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/connectors/graphql/src/test/resources/requests/fail-cases-connection-timeout-validation.json b/connectors/graphql/src/test/resources/requests/fail-cases-connection-timeout-validation.json new file mode 100644 index 0000000000..9098aecca7 --- /dev/null +++ b/connectors/graphql/src/test/resources/requests/fail-cases-connection-timeout-validation.json @@ -0,0 +1,103 @@ +[ + { + "descriptionOfTest": "Negative value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "-1", + "authentication": { + "type": "noAuth" + } + } + }, + { + "descriptionOfTest": "Negative value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "-99", + "authentication": { + "type": "noAuth" + } + } + }, + { + "descriptionOfTest": "String value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "bad value", + "authentication": { + "type": "noAuth" + } + } + }, + { + "descriptionOfTest": "Normal value with space", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "23 ", + "authentication": { + "type": "noAuth" + } + } + }, + { + "descriptionOfTest": "Blank value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": " ", + "authentication": { + "type": "noAuth" + } + } + }, + { + "descriptionOfTest": "Float value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "1.56", + "authentication": { + "type": "bearer", + "token": "secrets.TOKEN_KEY" + } + } + }, + { + "descriptionOfTest": "Float value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "0.1", + "authentication": { + "type": "noAuth" + } + } + }, + { + "descriptionOfTest": "Binary value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "0xFF", + "authentication": { + "type": "noAuth" + } + } + }, + { + "descriptionOfTest": "Binary value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "0b0011", + "authentication": { + "type": "bearer", + "token": "secrets.TOKEN_KEY" + } + } + } +] \ No newline at end of file diff --git a/connectors/graphql/src/test/resources/requests/fail-cases-request-without-one-required-field.json b/connectors/graphql/src/test/resources/requests/fail-cases-request-without-one-required-field.json new file mode 100644 index 0000000000..e5770cb394 --- /dev/null +++ b/connectors/graphql/src/test/resources/requests/fail-cases-request-without-one-required-field.json @@ -0,0 +1,178 @@ +[ + { + "testDescription": "without method field", + "authentication": { + "type": "noAuth" + }, + "graphql": { + "url": "testmail@testmail.com", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "testDescription": "without URL field", + "authentication": { + "type": "noAuth" + }, + "graphql": { + "method": "get", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "testDescription": "bearer auth type without URL field", + "authentication": { + "type": "bearer" + }, + "graphql": { + "method": "get", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "testDescription": "basic auth type without URL field", + "authentication": { + "type": "basic" + }, + "graphql": { + "method": "get", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "testDescription": "bearer auth without token", + "authentication": { + "type": "bearer" + }, + "graphql": { + "method": "get", + "url": "testmail@testmail.com", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "testDescription": "basic auth without password", + "authentication": { + "type": "basic", + "username": "username" + }, + "graphql": { + "method": "get", + "url": "testmail@testmail.com", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "testDescription": "basic auth without username", + "authentication": { + "type": "basic", + "password": "password" + }, + "graphql": { + "method": "get", + "url": "testmail@testmail.com", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "oauth request without client id", + "authentication":{ + "oauthTokenEndpoint":"https://abc.eu.auth0.com/api/v2/", + "scopes": "read:clients", + "audience":"https://abc.eu.auth0.com/api/v2/", + "clientSecret":"secrets.CLIENT_SECRET", + "type":"oauth-client-credentials-flow", + "clientAuthentication":"basicAuthHeader" + }, + "graphql": { + "method": "post", + "url": "https://abc/def", + "connectionTimeoutInSeconds": "30", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "oauth request without client secret", + "authentication":{ + "oauthTokenEndpoint":"https://abc.eu.auth0.com/api/v2/", + "scopes": "read:clients", + "audience":"https://abc.eu.auth0.com/api/v2/", + "clientId":"secrets.CLIENT_ID", + "type":"oauth-client-credentials-flow", + "clientAuthentication":"basicAuthHeader" + }, + "graphql": { + "method": "post", + "url": "https://abc/def", + "connectionTimeoutInSeconds": "30", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "oauth request without oauth token endpoint", + "authentication":{ + "scopes": "read:clients", + "audience":"https://abc.eu.auth0.com/api/v2/", + "clientId":"secrets.CLIENT_ID", + "clientSecret":"secrets.CLIENT_SECRET", + "type":"oauth-client-credentials-flow", + "clientAuthentication":"basicAuthHeader" + }, + "graphql": { + "method": "post", + "url": "https://abc/def", + "connectionTimeoutInSeconds": "30", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "oauth request without client authentication", + "authentication":{ + "oauthTokenEndpoint":"https://abc.eu.auth0.com/api/v2/", + "scopes": "read:clients", + "audience":"https://abc.eu.auth0.com/api/v2/", + "clientId":"secrets.CLIENT_ID", + "clientSecret":"secrets.CLIENT_SECRET", + "type":"oauth-client-credentials-flow" + }, + "graphql": { + "method": "post", + "url": "https://abc/def", + "connectionTimeoutInSeconds": "30", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + } +] \ No newline at end of file diff --git a/connectors/graphql/src/test/resources/requests/fail-test-cases.json b/connectors/graphql/src/test/resources/requests/fail-test-cases.json new file mode 100644 index 0000000000..4497488fc2 --- /dev/null +++ b/connectors/graphql/src/test/resources/requests/fail-test-cases.json @@ -0,0 +1,33 @@ +[ + { + "descriptionOfTest": "No method", + "graphql": { + "url": "https://camunda.io/http-endpoint", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "No URL", + "graphql": { + "method": "get", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "Malformed URL", + "graphql": { + "method": "get", + "url": "testmail@testmail.com", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + } +] \ No newline at end of file diff --git a/connectors/graphql/src/test/resources/requests/success-cases-connection-timeout-validation.json b/connectors/graphql/src/test/resources/requests/success-cases-connection-timeout-validation.json new file mode 100644 index 0000000000..5a839b3d3a --- /dev/null +++ b/connectors/graphql/src/test/resources/requests/success-cases-connection-timeout-validation.json @@ -0,0 +1,53 @@ +[ + { + "descriptionOfTest": "0 value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "connectionTimeoutInSeconds": "0" + }, + "authentication": { + "type": "noAuth" + } + }, + { + "descriptionOfTest": "0 value with bearer auth", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "connectionTimeoutInSeconds": "0" + }, + "authentication": { + "type": "bearer", + "token": "secrets.TOKEN_KEY" + } + }, + { + "descriptionOfTest": "positive value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "connectionTimeoutInSeconds": "99" + }, + "authentication": { + "type": "bearer", + "token": "secrets.TOKEN_KEY" + } + }, + { + "descriptionOfTest": "secrets value", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "connectionTimeoutInSeconds": "secrets.SOME_SECRET_KEY" + }, + "authentication": { + "type": "bearer", + "token": "secrets.TOKEN_KEY" + } + } +] \ No newline at end of file diff --git a/connectors/graphql/src/test/resources/requests/success-cases-replace-secrets.json b/connectors/graphql/src/test/resources/requests/success-cases-replace-secrets.json new file mode 100644 index 0000000000..901ae3db00 --- /dev/null +++ b/connectors/graphql/src/test/resources/requests/success-cases-replace-secrets.json @@ -0,0 +1,67 @@ +[ + { + "graphql": { + "url": "secrets.URL_KEY", + "method": "secrets.METHOD_KEY", + "connectionTimeoutInSeconds":"secrets.CONNECT_TIMEOUT_KEY", + "query": "query Root($id: {{secrets.QUERY_ID}}) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "{{secrets.VARIABLE_ID}}" + } + }, + "authentication": { + "type": "noAuth" + } + }, + { + "graphql": { + "url": "secrets.URL_KEY", + "method": "secrets.METHOD_KEY", + "connectionTimeoutInSeconds":"secrets.CONNECT_TIMEOUT_KEY", + "query": "query Root($id: {{secrets.QUERY_ID}}) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "{{secrets.VARIABLE_ID}}" + } + }, + "authentication": { + "type": "bearer", + "token": "secrets.TOKEN_KEY" + } + }, + { + "graphql": { + "url": "secrets.URL_KEY", + "method": "secrets.METHOD_KEY", + "connectionTimeoutInSeconds":"secrets.CONNECT_TIMEOUT_KEY", + "query": "query Root($id: {{secrets.QUERY_ID}}) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "{{secrets.VARIABLE_ID}}" + } + }, + "authentication": { + "type": "basic", + "password": "secrets.PASSWORD_KEY", + "username": "secrets.USERNAME_KEY" + } + }, + { + "graphql": { + "url": "secrets.URL_KEY", + "method": "secrets.METHOD_KEY", + "connectionTimeoutInSeconds":"secrets.CONNECT_TIMEOUT_KEY", + "query": "query Root($id: {{secrets.QUERY_ID}}) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "{{secrets.VARIABLE_ID}}" + } + }, + "authentication": { + "oauthTokenEndpoint":"secrets.OAUTH_TOKEN_ENDPOINT_KEY", + "scopes": "test", + "audience":"secrets.AUDIENCE_KEY", + "clientId":"secrets.CLIENT_ID_KEY", + "clientSecret":"secrets.CLIENT_SECRET_KEY", + "type": "oauth-client-credentials-flow", + "clientAuthentication":"secrets.CLIENT_AUTHENTICATION_KEY" + } + } +] \ No newline at end of file diff --git a/connectors/graphql/src/test/resources/requests/success-test-cases-oauth.json b/connectors/graphql/src/test/resources/requests/success-test-cases-oauth.json new file mode 100644 index 0000000000..8286e07c5d --- /dev/null +++ b/connectors/graphql/src/test/resources/requests/success-test-cases-oauth.json @@ -0,0 +1,22 @@ +[{ + "descriptionOfTest": "Normal request with oauth", + "authentication":{ + "oauthTokenEndpoint":"https://dev-test.eu.auth0.com/api/v2/", + "scopes": "read:clients", + "audience":"https://dev-test.eu.auth0.com/api/v2/", + "clientId":"secrets.CLIENT_ID", + "clientSecret":"secrets.CLIENT_SECRET", + "type":"oauth-client-credentials-flow", + "clientAuthentication":"basicAuthHeader" + }, + "graphql": { + "method": "post", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "30", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } +} +] \ No newline at end of file diff --git a/connectors/graphql/src/test/resources/requests/success-test-cases.json b/connectors/graphql/src/test/resources/requests/success-test-cases.json new file mode 100644 index 0000000000..23f95d02fe --- /dev/null +++ b/connectors/graphql/src/test/resources/requests/success-test-cases.json @@ -0,0 +1,88 @@ +[ + { + "descriptionOfTest": "Normal request with no auth", + "authentication": { + "type": "noAuth" + }, + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "20", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "Normal request with basic auth", + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "0", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "Normal request with bearer auth", + "authentication": { + "type": "bearer", + "token": "secrets.MY_TOKEN" + }, + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "30", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + }, + { + "descriptionOfTest": "Normal request with no variables", + "authentication": { + "type": "bearer", + "token": "secrets.MY_TOKEN" + }, + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "200", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}" + } + }, + { + "descriptionOfTest": "Normal request with empty variables", + "authentication": { + "type": "bearer", + "token": "secrets.MY_TOKEN" + }, + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "0", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": {} + } + }, + { + "descriptionOfTest": "Normal request with connectionTimeoutInSeconds", + "authentication": { + "type": "bearer", + "token": "secrets.MY_TOKEN" + }, + "graphql": { + "method": "get", + "url": "https://camunda.io/http-endpoint", + "connectionTimeoutInSeconds": "50", + "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}", + "variables": { + "id": "cGVvcGxlOjI=" + } + } + } +] diff --git a/connectors/http-json/pom.xml b/connectors/http-json/pom.xml index 01e5d54844..38b6cab23a 100644 --- a/connectors/http-json/pom.xml +++ b/connectors/http-json/pom.xml @@ -62,6 +62,11 @@ limitations under the License. org.slf4j jcl-over-slf4j + + + io.camunda.connector + connectors-common-library + diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpJsonFunction.java b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpJsonFunction.java index 6bd71835a5..4c609728eb 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpJsonFunction.java +++ b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpJsonFunction.java @@ -21,9 +21,9 @@ import io.camunda.connector.api.annotation.OutboundConnector; import io.camunda.connector.api.outbound.OutboundConnectorContext; import io.camunda.connector.api.outbound.OutboundConnectorFunction; +import io.camunda.connector.common.constants.Constants; import io.camunda.connector.http.components.GsonComponentSupplier; import io.camunda.connector.http.components.HttpTransportComponentSupplier; -import io.camunda.connector.http.constants.Constants; import io.camunda.connector.http.model.HttpJsonRequest; import io.camunda.connector.impl.config.ConnectorConfigurationUtil; import java.io.IOException; @@ -63,7 +63,8 @@ public HttpJsonFunction( } @Override - public Object execute(final OutboundConnectorContext context) throws IOException { + public Object execute(final OutboundConnectorContext context) + throws IOException, InstantiationException, IllegalAccessException { final var json = context.getVariables(); final var request = gson.fromJson(json, HttpJsonRequest.class); diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpRequestMapper.java b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpRequestMapper.java index 2de84be953..d55d7dea62 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpRequestMapper.java +++ b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpRequestMapper.java @@ -18,25 +18,23 @@ import static org.apache.http.entity.ContentType.APPLICATION_JSON; -import com.google.api.client.http.AbstractHttpContent; import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.UrlEncodedContent; import com.google.api.client.http.json.JsonHttpContent; import com.google.gson.Gson; -import io.camunda.connector.http.auth.OAuthAuthentication; -import io.camunda.connector.http.auth.ProxyOAuthHelper; +import io.camunda.connector.common.auth.OAuthAuthentication; +import io.camunda.connector.common.constants.Constants; +import io.camunda.connector.common.model.CommonRequest; +import io.camunda.connector.common.model.HttpRequestBuilder; import io.camunda.connector.http.components.GsonComponentSupplier; -import io.camunda.connector.http.constants.Constants; import io.camunda.connector.http.model.HttpJsonRequest; -import io.camunda.connector.http.model.HttpRequestBuilder; import io.camunda.connector.impl.ConnectorInputException; import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import javax.validation.ValidationException; public class HttpRequestMapper { @@ -45,35 +43,6 @@ public class HttpRequestMapper { private HttpRequestMapper() {} - public static HttpRequest toRequestViaProxy( - final HttpRequestFactory requestFactory, - final HttpJsonRequest request, - final String proxyFunctionUrl) - throws IOException { - // Using the JsonHttpContent cannot work with an element on the root content, - // hence write it ourselves: - final String contentAsJson = gson.toJson(request); - HttpContent content = - new AbstractHttpContent(Constants.APPLICATION_JSON_CHARSET_UTF_8) { - public void writeTo(OutputStream outputStream) throws IOException { - outputStream.write(contentAsJson.getBytes(StandardCharsets.UTF_8)); - } - }; - - HttpRequest httpRequest = - new HttpRequestBuilder() - .method(Constants.POST) - .genericUrl(new GenericUrl(proxyFunctionUrl)) - .content(content) - .connectionTimeoutInSeconds(request.getConnectionTimeoutInSeconds()) - .followRedirects(false) - .build(requestFactory); - - ProxyOAuthHelper.addOauthHeaders( - httpRequest, ProxyOAuthHelper.initializeCredentials(proxyFunctionUrl)); - return httpRequest; - } - public static HttpRequest toOAuthHttpRequest( final HttpRequestFactory requestFactory, final HttpJsonRequest request) throws IOException { @@ -89,21 +58,34 @@ public static HttpRequest toOAuthHttpRequest( return new HttpRequestBuilder() .method(Constants.POST) .genericUrl(new GenericUrl(authentication.getOauthTokenEndpoint())) - .content(new UrlEncodedContent(authentication.getDataForAuthRequestBody())) + .content(new UrlEncodedContent(getDataForAuthRequestBody(authentication))) .headers(headers) .connectionTimeoutInSeconds(request.getConnectionTimeoutInSeconds()) .followRedirects(false) .build(requestFactory); } + public static Map getDataForAuthRequestBody(OAuthAuthentication authentication) { + Map data = new HashMap<>(); + data.put(Constants.GRANT_TYPE, authentication.getGrantType()); + data.put(Constants.AUDIENCE, authentication.getAudience()); + data.put(Constants.SCOPE, authentication.getScopes()); + + if (Constants.CREDENTIALS_BODY.equals(authentication.getClientAuthentication())) { + data.put(Constants.CLIENT_ID, authentication.getClientId()); + data.put(Constants.CLIENT_SECRET, authentication.getClientSecret()); + } + return data; + } + public static HttpRequest toHttpRequest( - final HttpRequestFactory requestFactory, final HttpJsonRequest request) throws IOException { + final HttpRequestFactory requestFactory, final CommonRequest request) throws IOException { return toHttpRequest(requestFactory, request, null); } public static HttpRequest toHttpRequest( final HttpRequestFactory requestFactory, - final HttpJsonRequest request, + final CommonRequest request, final String bearerToken) throws IOException { // TODO: add more holistic solution @@ -131,7 +113,7 @@ public static HttpRequest toHttpRequest( .build(requestFactory); } - private static HttpHeaders createHeaders(final HttpJsonRequest request, String bearerToken) { + private static HttpHeaders createHeaders(final CommonRequest request, String bearerToken) { final HttpHeaders httpHeaders = new HttpHeaders(); if (request.hasBody()) { httpHeaders.setContentType(APPLICATION_JSON.getMimeType()); diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpResponseMapper.java b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpResponseMapper.java deleted file mode 100644 index 8efbaf46c7..0000000000 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpResponseMapper.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. Camunda licenses this file to you under the Apache License, - * Version 2.0; 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 io.camunda.connector.http; - -import com.google.api.client.http.HttpResponse; -import com.google.gson.Gson; -import io.camunda.connector.http.components.GsonComponentSupplier; -import io.camunda.connector.http.model.HttpJsonResult; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class HttpResponseMapper { - - private static final Logger LOGGER = LoggerFactory.getLogger(HttpResponseMapper.class); - private static final Gson gson = GsonComponentSupplier.gsonInstance(); - - private HttpResponseMapper() {} - - public static HttpJsonResult toHttpJsonResponse(final HttpResponse externalResponse) { - final HttpJsonResult httpJsonResult = new HttpJsonResult(); - httpJsonResult.setStatus(externalResponse.getStatusCode()); - final Map headers = new HashMap<>(); - externalResponse - .getHeaders() - .forEach( - (k, v) -> { - if (v instanceof List && ((List) v).size() == 1) { - headers.put(k, ((List) v).get(0)); - } else { - headers.put(k, v); - } - }); - httpJsonResult.setHeaders(headers); - try (InputStream content = externalResponse.getContent(); - Reader reader = new InputStreamReader(content)) { - final Object body = gson.fromJson(reader, Object.class); - if (body != null) { - httpJsonResult.setBody(body); - } - } catch (final Exception e) { - LOGGER.error("Failed to parse external response: {}", externalResponse, e); - } - return httpJsonResult; - } -} diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpService.java b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpService.java index 220ee5e3d9..3caafa5f9c 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpService.java +++ b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpService.java @@ -19,21 +19,19 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.gson.Gson; -import com.google.gson.JsonObject; import io.camunda.connector.api.error.ConnectorException; -import io.camunda.connector.http.auth.CustomAuthentication; -import io.camunda.connector.http.auth.OAuthAuthentication; -import io.camunda.connector.http.model.ErrorResponse; +import io.camunda.connector.common.auth.CustomAuthentication; +import io.camunda.connector.common.auth.OAuthAuthentication; +import io.camunda.connector.common.services.AuthenticationService; +import io.camunda.connector.common.services.HTTPProxyService; +import io.camunda.connector.common.services.HTTPService; import io.camunda.connector.http.model.HttpJsonRequest; import io.camunda.connector.http.model.HttpJsonResult; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.util.HashMap; -import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,24 +49,28 @@ public HttpService( this.proxyFunctionUrl = proxyFunctionUrl; } - public Object executeConnectorRequest(final HttpJsonRequest request) throws IOException { + public Object executeConnectorRequest(final HttpJsonRequest request) + throws IOException, InstantiationException, IllegalAccessException { return proxyFunctionUrl == null ? executeRequestDirectly(request) : executeRequestViaProxy(request); } - private HttpJsonResult executeRequestDirectly(HttpJsonRequest request) throws IOException { + private HttpJsonResult executeRequestDirectly(HttpJsonRequest request) + throws IOException, InstantiationException, IllegalAccessException { String bearerToken = null; + HTTPService httpService = new HTTPService(gson); + AuthenticationService authService = new AuthenticationService(gson, requestFactory); if (request.getAuthentication() != null) { if (request.getAuthentication() instanceof OAuthAuthentication) { - bearerToken = getTokenFromOAuthRequest(request); + bearerToken = getTokenFromOAuthRequest(request, httpService, authService); } else if (request.getAuthentication() instanceof CustomAuthentication) { final var authentication = (CustomAuthentication) request.getAuthentication(); final var httpRequest = HttpRequestMapper.toHttpRequest(requestFactory, authentication.getRequest()); - HttpResponse httpResponse = executeHttpRequest(httpRequest); + HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest); if (httpResponse.isSuccessStatusCode()) { - fillRequestFromCustomAuthResponseData(request, authentication, httpResponse); + authService.fillRequestFromCustomAuthResponseData(request, authentication, httpResponse); } else { throw new RuntimeException( "Authenticate is fail; status code : [" @@ -80,74 +82,27 @@ private HttpJsonResult executeRequestDirectly(HttpJsonRequest request) throws IO } } HttpRequest httpRequest = HttpRequestMapper.toHttpRequest(requestFactory, request, bearerToken); - HttpResponse httpResponse = executeHttpRequest(httpRequest, false); - return HttpResponseMapper.toHttpJsonResponse(httpResponse); + HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest, false); + return httpService.toHttpJsonResponse(httpResponse, HttpJsonResult.class); } - private void fillRequestFromCustomAuthResponseData( - final HttpJsonRequest request, - final CustomAuthentication authentication, - final HttpResponse httpResponse) + private String getTokenFromOAuthRequest( + final HttpJsonRequest connectorRequest, + final HTTPService httpService, + final AuthenticationService authService) throws IOException { - String strResponse = httpResponse.parseAsString(); - Map headers = - ResponseParser.extractPropertiesFromBody(authentication.getOutputHeaders(), strResponse); - if (headers != null) { - if (!request.hasHeaders()) { - request.setHeaders(new HashMap<>()); - } - request.getHeaders().putAll(headers); - } - - Map body = - ResponseParser.extractPropertiesFromBody(authentication.getOutputBody(), strResponse); - if (body != null) { - if (!request.hasBody()) { - request.setBody(new Object()); - } - JsonObject requestBody = gson.toJsonTree(request.getBody()).getAsJsonObject(); - // for now, we can add only string property to body, example of this object : - // "{"key":"value"}" but we can expand this method - body.forEach(requestBody::addProperty); - request.setBody(gson.fromJson(gson.toJson(requestBody), Object.class)); - } - } - - private String getTokenFromOAuthRequest(final HttpJsonRequest request) throws IOException { - final HttpRequest httpRequest = HttpRequestMapper.toOAuthHttpRequest(requestFactory, request); - final HttpResponse oauthResponse = executeHttpRequest(httpRequest); - return ResponseParser.extractOAuthAccessToken(oauthResponse); - } - - private HttpResponse executeHttpRequest(HttpRequest externalRequest) throws IOException { - return executeHttpRequest(externalRequest, false); - } - - private HttpResponse executeHttpRequest(HttpRequest externalRequest, boolean isProxyCall) - throws IOException { - try { - return externalRequest.execute(); - } catch (HttpResponseException hrex) { - var errorCode = String.valueOf(hrex.getStatusCode()); - var errorMessage = hrex.getMessage(); - if (isProxyCall && hrex.getContent() != null) { - try { - final var errorContent = gson.fromJson(hrex.getContent(), ErrorResponse.class); - errorCode = errorContent.getErrorCode(); - errorMessage = errorContent.getError(); - } catch (Exception e) { - // cannot be loaded as JSON, ignore and use plain message - } - } - throw new ConnectorException(errorCode, errorMessage, hrex); - } + final HttpRequest oauthRequest = authService.createOAuthRequest(connectorRequest); + final HttpResponse oauthResponse = httpService.executeHttpRequest(oauthRequest); + return authService.extractOAuthAccessToken(oauthResponse); } private HttpJsonResult executeRequestViaProxy(HttpJsonRequest request) throws IOException { HttpRequest httpRequest = - HttpRequestMapper.toRequestViaProxy(requestFactory, request, proxyFunctionUrl); + HTTPProxyService.toRequestViaProxy(gson, requestFactory, request, proxyFunctionUrl); + + HTTPService httpService = new HTTPService(gson); - HttpResponse httpResponse = executeHttpRequest(httpRequest, true); + HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest, true); try (InputStream responseContentStream = httpResponse.getContent(); Reader reader = new InputStreamReader(responseContentStream)) { diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/components/GsonComponentSupplier.java b/connectors/http-json/src/main/java/io/camunda/connector/http/components/GsonComponentSupplier.java index 28efdd427d..e590087277 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/components/GsonComponentSupplier.java +++ b/connectors/http-json/src/main/java/io/camunda/connector/http/components/GsonComponentSupplier.java @@ -21,12 +21,12 @@ import com.google.gson.GsonBuilder; import com.google.gson.ToNumberPolicy; import com.google.gson.typeadapters.RuntimeTypeAdapterFactory; -import io.camunda.connector.http.auth.Authentication; -import io.camunda.connector.http.auth.BasicAuthentication; -import io.camunda.connector.http.auth.BearerAuthentication; -import io.camunda.connector.http.auth.CustomAuthentication; -import io.camunda.connector.http.auth.NoAuthentication; -import io.camunda.connector.http.auth.OAuthAuthentication; +import io.camunda.connector.common.auth.Authentication; +import io.camunda.connector.common.auth.BasicAuthentication; +import io.camunda.connector.common.auth.BearerAuthentication; +import io.camunda.connector.common.auth.CustomAuthentication; +import io.camunda.connector.common.auth.NoAuthentication; +import io.camunda.connector.common.auth.OAuthAuthentication; public class GsonComponentSupplier { diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonRequest.java b/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonRequest.java index a407e5f540..5f8ac6b623 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonRequest.java +++ b/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonRequest.java @@ -16,148 +16,6 @@ */ package io.camunda.connector.http.model; -import com.google.common.base.Objects; -import io.camunda.connector.api.annotation.Secret; -import io.camunda.connector.http.auth.Authentication; -import java.util.Map; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Pattern; +import io.camunda.connector.common.model.CommonRequest; -public class HttpJsonRequest { - - @NotBlank @Secret private String method; - - @NotBlank - @Pattern(regexp = "^(http://|https://|secrets).*$") - @Secret - private String url; - - @Valid @Secret private Authentication authentication; - @Secret private Map queryParameters; - @Secret private Map headers; - - @Pattern(regexp = "^([0-9]*$)|(secrets.*$)") - @Secret - private String connectionTimeoutInSeconds; - - @Secret private Object body; - - public boolean hasAuthentication() { - return authentication != null; - } - - public boolean hasQueryParameters() { - return queryParameters != null; - } - - public boolean hasHeaders() { - return headers != null; - } - - public boolean hasBody() { - return body != null; - } - - public String getMethod() { - return method; - } - - public void setMethod(final String method) { - this.method = method; - } - - public String getUrl() { - return url; - } - - public void setUrl(final String url) { - this.url = url; - } - - public Authentication getAuthentication() { - return authentication; - } - - public void setAuthentication(final Authentication authentication) { - this.authentication = authentication; - } - - public Map getQueryParameters() { - return queryParameters; - } - - public void setQueryParameters(final Map queryParameters) { - this.queryParameters = queryParameters; - } - - public Map getHeaders() { - return headers; - } - - public void setHeaders(final Map headers) { - this.headers = headers; - } - - public String getConnectionTimeoutInSeconds() { - return connectionTimeoutInSeconds; - } - - public void setConnectionTimeoutInSeconds(final String connectionTimeoutInSeconds) { - this.connectionTimeoutInSeconds = connectionTimeoutInSeconds; - } - - public Object getBody() { - return body; - } - - public void setBody(final Object body) { - this.body = body; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final HttpJsonRequest that = (HttpJsonRequest) o; - return java.util.Objects.equals(method, that.method) - && java.util.Objects.equals(url, that.url) - && java.util.Objects.equals(authentication, that.authentication) - && java.util.Objects.equals(queryParameters, that.queryParameters) - && java.util.Objects.equals(headers, that.headers) - && java.util.Objects.equals(connectionTimeoutInSeconds, that.connectionTimeoutInSeconds) - && java.util.Objects.equals(body, that.body); - } - - @Override - public int hashCode() { - return Objects.hashCode(method, url, authentication, queryParameters, headers, body); - } - - @Override - public String toString() { - return "HttpJsonRequest{" - + "method='" - + method - + '\'' - + ", url='" - + url - + '\'' - + ", authentication=" - + authentication - + ", queryParameters=" - + queryParameters - + ", headers=" - + headers - + ", connectionTimeoutInSeconds='" - + connectionTimeoutInSeconds - + '\'' - + ", body=" - + body - + '}'; - } -} +public class HttpJsonRequest extends CommonRequest {} diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonResult.java b/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonResult.java index 22c22e08c2..5d208084d1 100644 --- a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonResult.java +++ b/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonResult.java @@ -16,59 +16,6 @@ */ package io.camunda.connector.http.model; -import com.google.common.base.Objects; -import java.util.Map; +import io.camunda.connector.common.model.CommonResult; -public class HttpJsonResult { - private int status; - private Map headers; - private Object body; - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public Map getHeaders() { - return headers; - } - - public void setHeaders(Map headers) { - this.headers = headers; - } - - public Object getBody() { - return body; - } - - public void setBody(Object body) { - this.body = body; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - HttpJsonResult that = (HttpJsonResult) o; - return status == that.status - && Objects.equal(headers, that.headers) - && Objects.equal(body, that.body); - } - - @Override - public int hashCode() { - return Objects.hashCode(status, headers, body); - } - - @Override - public String toString() { - return "HttpJsonResult{" + "status=" + status + ", headers=" + headers + ", body=" + body + '}'; - } -} +public class HttpJsonResult extends CommonResult {} diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionProxyTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionProxyTest.java index 16fd187198..a99d49a607 100644 --- a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionProxyTest.java +++ b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionProxyTest.java @@ -32,7 +32,7 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import io.camunda.connector.api.error.ConnectorException; -import io.camunda.connector.http.constants.Constants; +import io.camunda.connector.common.constants.Constants; import io.camunda.connector.http.model.HttpJsonResult; import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder; import java.io.ByteArrayInputStream; @@ -67,7 +67,8 @@ public void setup() { @ParameterizedTest(name = "Executing test case: {0}") @MethodSource("successCases") - void shouldReturnResult_WhenExecuted(final String input) throws IOException { + void shouldReturnResult_WhenExecuted(final String input) + throws IOException, InstantiationException, IllegalAccessException { // given - minimal required entity final var context = OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build(); diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionSecretsTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionSecretsTest.java index 1250f7dc61..b9291c3d67 100644 --- a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionSecretsTest.java +++ b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionSecretsTest.java @@ -21,11 +21,11 @@ import com.google.gson.JsonObject; import io.camunda.connector.api.outbound.OutboundConnectorContext; -import io.camunda.connector.http.auth.Authentication; -import io.camunda.connector.http.auth.BasicAuthentication; -import io.camunda.connector.http.auth.BearerAuthentication; -import io.camunda.connector.http.auth.NoAuthentication; -import io.camunda.connector.http.auth.OAuthAuthentication; +import io.camunda.connector.common.auth.Authentication; +import io.camunda.connector.common.auth.BasicAuthentication; +import io.camunda.connector.common.auth.BearerAuthentication; +import io.camunda.connector.common.auth.NoAuthentication; +import io.camunda.connector.common.auth.OAuthAuthentication; import io.camunda.connector.http.model.HttpJsonRequest; import java.io.IOException; import java.util.stream.Stream; diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionTest.java index 2272d4dffd..ce3b68b661 100644 --- a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionTest.java +++ b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionTest.java @@ -79,7 +79,8 @@ public void setup() { @ParameterizedTest(name = "Executing test case: {0}") @MethodSource("successCases") - void shouldReturnResult_WhenExecuted(final String input) throws IOException { + void shouldReturnResult_WhenExecuted(final String input) + throws IOException, InstantiationException, IllegalAccessException { // given - minimal required entity Object functionCallResponseAsObject = arrange(input); @@ -92,7 +93,8 @@ void shouldReturnResult_WhenExecuted(final String input) throws IOException { @ParameterizedTest(name = "Executing test case: {0}") @MethodSource("successCasesCustomAuth") - void shouldReturnResultCustom_WhenExecuted(final String input) throws IOException { + void shouldReturnResultCustom_WhenExecuted(final String input) + throws IOException, InstantiationException, IllegalAccessException { String response = "{\"token\":\"eyJhbJNtIbehBWQLAGapcHIctws7gavjTCSCCC0Xd5sIn7DaB52Pwmabdj-9AkrVru_fZwLQseAq38n1-DkiyAaewxB0VbQgQ\",\"user\":{\"id\":331707,\"principalId\":331707,\"deleted\":false,\"permissions\":[{\"id\":13044559,\"resourceType\":\"processdiscovery\"},{\"id\":13044527,\"resourceType\":\"credentials\"},],\"emailVerified\":true,\"passwordSet\":true},\"tenantUuid\":\"08b93cfe-a6dd-4d6b-94aa-9369fdd2a026\"}"; @@ -109,7 +111,8 @@ void shouldReturnResultCustom_WhenExecuted(final String input) throws IOExceptio @ParameterizedTest(name = "Executing test case: {0}") @MethodSource("successCasesOauth") - void shouldReturnResultOAuth_WhenExecuted(final String input) throws IOException { + void shouldReturnResultOAuth_WhenExecuted(final String input) + throws IOException, InstantiationException, IllegalAccessException { Object functionCallResponseAsObject = arrange(input); // then @@ -119,7 +122,8 @@ void shouldReturnResultOAuth_WhenExecuted(final String input) throws IOException .containsValue(APPLICATION_JSON.getMimeType()); } - private Object arrange(String input) throws IOException { + private Object arrange(String input) + throws IOException, InstantiationException, IllegalAccessException { final var context = OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build(); when(requestFactory.buildRequest( @@ -149,7 +153,8 @@ void shouldReturnFallbackResult_WhenMalformedRequest(final String input) { } @Test - void execute_shouldReturnNullFieldWhenResponseWithContainNullField() throws IOException { + void execute_shouldReturnNullFieldWhenResponseWithContainNullField() + throws IOException, InstantiationException, IllegalAccessException { // given request, and response body with null field value final var request = "{ \"method\": \"get\", \"url\": \"https://camunda.io/http-endpoint\", \"authentication\": { \"type\": \"noAuth\" } }"; @@ -177,7 +182,8 @@ void execute_shouldReturnNullFieldWhenResponseWithContainNullField() throws IOEx @ParameterizedTest(name = "Executing test case: {0}") @MethodSource("successCases") - void execute_shouldSetConnectTime(final String input) throws IOException { + void execute_shouldSetConnectTime(final String input) + throws IOException, InstantiationException, IllegalAccessException { // given - minimal required entity final var context = OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build(); diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpServiceTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpServiceTest.java index 6ddce91274..e141ec4d37 100644 --- a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpServiceTest.java +++ b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpServiceTest.java @@ -33,7 +33,8 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.api.client.testing.http.MockLowLevelHttpResponse; -import io.camunda.connector.http.constants.Constants; +import io.camunda.connector.common.constants.Constants; +import io.camunda.connector.common.services.AuthenticationService; import io.camunda.connector.http.model.HttpJsonRequest; import io.camunda.connector.http.model.HttpJsonResult; import io.camunda.connector.impl.config.ConnectorConfigurationUtil; @@ -78,7 +79,8 @@ void checkIfOAuthBearerTokenIsAddedOnTheRequestHeader(final String input) throws when(httpResponse.parseAsString()).thenReturn(ACCESS_TOKEN); // when - String bearerToken = ResponseParser.extractOAuthAccessToken(httpResponse); + AuthenticationService authenticationService = new AuthenticationService(gson, requestFactory); + String bearerToken = authenticationService.extractOAuthAccessToken(httpResponse); HttpRequest request = HttpRequestMapper.toHttpRequest(requestFactory, httpJsonRequest, bearerToken); // check if the bearer token is correctly added on the header of the main request @@ -88,7 +90,8 @@ void checkIfOAuthBearerTokenIsAddedOnTheRequestHeader(final String input) throws @ParameterizedTest(name = "Executing test case: {0}") @MethodSource("successCasesCustomAuth") - void execute_shouldPassAllStepsAndParsing(final String input) throws IOException { + void execute_shouldPassAllStepsAndParsing(final String input) + throws IOException, InstantiationException, IllegalAccessException { // given final var context = OutboundConnectorContextBuilder.create().variables(input).build(); final var httpJsonRequest = gson.fromJson(context.getVariables(), HttpJsonRequest.class); diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/auth/OAuthAuthenticationTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/auth/OAuthAuthenticationTest.java index 54660f9b89..6c299c582a 100644 --- a/connectors/http-json/src/test/java/io/camunda/connector/http/auth/OAuthAuthenticationTest.java +++ b/connectors/http-json/src/test/java/io/camunda/connector/http/auth/OAuthAuthenticationTest.java @@ -27,10 +27,10 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.testing.http.MockHttpTransport; +import io.camunda.connector.common.constants.Constants; +import io.camunda.connector.common.services.AuthenticationService; import io.camunda.connector.http.BaseTest; import io.camunda.connector.http.HttpRequestMapper; -import io.camunda.connector.http.ResponseParser; -import io.camunda.connector.http.constants.Constants; import io.camunda.connector.http.model.HttpJsonRequest; import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder; import java.io.IOException; @@ -81,7 +81,8 @@ void checkOAuthBearerTokenFormat(final String input) throws IOException { // check if the bearer token has the correct format and doesn't contain quotes - assertFalse(ResponseParser.extractOAuthAccessToken(httpResponse).contains("\"")); + AuthenticationService authenticationService = new AuthenticationService(gson, requestFactory); + assertFalse(authenticationService.extractOAuthAccessToken(httpResponse).contains("\"")); } private static Stream successCasesOauth() throws IOException { diff --git a/connectors/pom.xml b/connectors/pom.xml index 3eb15d451d..e9fe6dcead 100644 --- a/connectors/pom.xml +++ b/connectors/pom.xml @@ -28,6 +28,8 @@ http-json microsoft-teams slack + graphql + connectors-common-library diff --git a/pom.xml b/pom.xml index e68737cdf6..81e9e7db36 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.camunda.connector connector-parent - 0.6.0-SNAPSHOT + 0.6.0-alpha3 @@ -183,6 +183,12 @@ ${version.slack} + + io.camunda.connector + connectors-common-library + ${project.version} + + org.apache.commons