diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProvider.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProvider.java index 638d0428..7dffc629 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProvider.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProvider.java @@ -18,10 +18,11 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; +import java.net.URI; public interface AuthProvider { - String authScheme(); + String scheme(); - String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model); + String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProviderFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProviderFactory.java index fb28f43b..9a0c20b5 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProviderFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProviderFactory.java @@ -31,11 +31,11 @@ public static Optional getAuth( WorkflowDefinition definition, EndpointConfiguration configuration) { return configuration == null ? Optional.empty() - : getAuth(definition, configuration.getAuthentication()); + : getAuth(definition, configuration.getAuthentication(), "GET"); } public static Optional getAuth( - WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth) { + WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth, String method) { if (auth == null) { return Optional.empty(); } @@ -43,24 +43,28 @@ public static Optional getAuth( return buildFromReference( definition.application(), definition.workflow(), - auth.getAuthenticationPolicyReference().getUse()); + auth.getAuthenticationPolicyReference().getUse(), + method); } else if (auth.getAuthenticationPolicy() != null) { return buildFromPolicy( - definition.application(), definition.workflow(), auth.getAuthenticationPolicy()); + definition.application(), definition.workflow(), auth.getAuthenticationPolicy(), method); } return Optional.empty(); } private static Optional buildFromReference( - WorkflowApplication app, Workflow workflow, String use) { + WorkflowApplication app, Workflow workflow, String use, String method) { return workflow.getUse().getAuthentications().getAdditionalProperties().entrySet().stream() .filter(s -> s.getKey().equals(use)) .findAny() - .flatMap(e -> buildFromPolicy(app, workflow, e.getValue())); + .flatMap(e -> buildFromPolicy(app, workflow, e.getValue(), method)); } private static Optional buildFromPolicy( - WorkflowApplication app, Workflow workflow, AuthenticationPolicyUnion authenticationPolicy) { + WorkflowApplication app, + Workflow workflow, + AuthenticationPolicyUnion authenticationPolicy, + String method) { if (authenticationPolicy.getBasicAuthenticationPolicy() != null) { return Optional.of( new BasicAuthProvider( @@ -70,8 +74,9 @@ private static Optional buildFromPolicy( new BearerAuthProvider( app, workflow, authenticationPolicy.getBearerAuthenticationPolicy())); } else if (authenticationPolicy.getDigestAuthenticationPolicy() != null) { - // TODO implement digest authentication - return Optional.empty(); + return Optional.of( + new DigestAuthProvider( + app, workflow, authenticationPolicy.getDigestAuthenticationPolicy(), method)); } else if (authenticationPolicy.getOAuth2AuthenticationPolicy() != null) { return Optional.of( new OAuth2AuthProvider( diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java index 7dc38882..95e57555 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java @@ -15,6 +15,8 @@ */ package io.serverlessworkflow.impl.auth; +import java.util.Random; + public class AuthUtils { private AuthUtils() {} @@ -38,7 +40,15 @@ private AuthUtils() {} private static final String AUTH_HEADER_FORMAT = "%s %s"; + private static class RandomHolder { + private static final Random random = new Random(); + } + public static String authHeaderValue(String scheme, String parameter) { return String.format(AUTH_HEADER_FORMAT, scheme, parameter); } + + public static String getRandomHexString() { + return String.format("%08x", RandomHolder.random.nextInt()); + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/BasicAuthProvider.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/BasicAuthProvider.java index b866b181..11694c74 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/BasicAuthProvider.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/BasicAuthProvider.java @@ -28,6 +28,7 @@ import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowUtils; import io.serverlessworkflow.impl.WorkflowValueResolver; +import java.net.URI; import java.util.Base64; class BasicAuthProvider implements AuthProvider { @@ -57,7 +58,7 @@ public BasicAuthProvider( } @Override - public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) { + public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { return new String( Base64.getEncoder() .encode( @@ -69,7 +70,7 @@ public String authParameter(WorkflowContext workflow, TaskContext task, Workflow } @Override - public String authScheme() { + public String scheme() { return "Basic"; } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/BearerAuthProvider.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/BearerAuthProvider.java index c77cf63d..b445c874 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/BearerAuthProvider.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/BearerAuthProvider.java @@ -28,6 +28,7 @@ import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowUtils; import io.serverlessworkflow.impl.WorkflowValueResolver; +import java.net.URI; class BearerAuthProvider implements AuthProvider { @@ -48,12 +49,12 @@ public BearerAuthProvider( } @Override - public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) { + public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { return tokenFilter.apply(workflow, task, model); } @Override - public String authScheme() { + public String scheme() { return "Bearer"; } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/CommonOAuthProvider.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/CommonOAuthProvider.java index bb97c8df..9ead4c59 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/CommonOAuthProvider.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/CommonOAuthProvider.java @@ -25,6 +25,7 @@ import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowValueResolver; +import java.net.URI; import java.util.Arrays; import java.util.Map; import java.util.ServiceLoader; @@ -48,12 +49,12 @@ protected CommonOAuthProvider(WorkflowValueResolver tokenPr } @Override - public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) { + public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { return tokenProvider.apply(workflow, task, model).validateAndGet(workflow, task, model).token(); } @Override - public String authScheme() { + public String scheme() { return "Bearer"; } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/DigestAuthProvider.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/DigestAuthProvider.java new file mode 100644 index 00000000..61517b59 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/DigestAuthProvider.java @@ -0,0 +1,252 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl.auth; + +import static io.serverlessworkflow.impl.WorkflowUtils.checkSecret; +import static io.serverlessworkflow.impl.WorkflowUtils.secretProp; +import static io.serverlessworkflow.impl.auth.AuthUtils.PASSWORD; +import static io.serverlessworkflow.impl.auth.AuthUtils.USER; + +import io.serverlessworkflow.api.types.DigestAuthenticationPolicy; +import io.serverlessworkflow.api.types.DigestAuthenticationProperties; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowUtils; +import io.serverlessworkflow.impl.WorkflowValueResolver; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Optional; +import java.util.StringTokenizer; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class DigestAuthProvider implements AuthProvider { + + private static final String NONCE = "nonce"; + private static final String REALM = "realm"; + private static final String QOP_KEY = "qop"; + private static final String OPAQUE = "opaque"; + private static final String DIGEST = "Digest"; + private static final AtomicInteger nc = new AtomicInteger(1); + + private static class DigestServerInfo { + + private Algorithm algorithm = Algorithm.MD5; + private String nonce; + private String opaque; + private String realm; + private Optional qop = Optional.empty(); + + private static final Pattern pattern = Pattern.compile("([A-Za-z]+)=\\\"([^\\\"]*)\\\",?"); + + public static DigestServerInfo from(String header) { + if (header == null || !header.startsWith(DIGEST)) { + throw new IllegalArgumentException("Invalid WWW-Authenticate header content: " + header); + } + DigestServerInfo serverInfo = new DigestServerInfo(); + Matcher m = pattern.matcher(header.substring(DIGEST.length()).trim()); + while (m.find()) { + String key = m.group(1); + String value = m.group(2).trim(); + switch (key) { + case "algorithm": + serverInfo.algorithm = Algorithm.valueOf(value.toUpperCase()); + break; + case NONCE: + serverInfo.nonce = value; + break; + case OPAQUE: + serverInfo.opaque = value; + break; + case REALM: + serverInfo.realm = value; + break; + case QOP_KEY: + StringTokenizer qopTokenizer = new StringTokenizer(value, ","); + while (qopTokenizer.hasMoreElements()) { + try { + serverInfo.qop = Optional.of(QOP.valueOf(qopTokenizer.nextToken().toUpperCase())); + break; + } catch (IllegalArgumentException ex) { + // search for next valid protocol + } + } + break; + } + } + + return serverInfo; + } + } + + private static enum Algorithm { + MD5, + MD5SESSS + }; + + private static enum QOP { + AUTH, + AUTH_INT, + }; + + private final WorkflowValueResolver userFilter; + private final WorkflowValueResolver passwordFilter; + private final String method; + + public DigestAuthProvider( + WorkflowApplication app, + Workflow workflow, + DigestAuthenticationPolicy authPolicy, + String method) { + DigestAuthenticationProperties properties = + authPolicy.getDigest().getDigestAuthenticationProperties(); + if (properties != null) { + userFilter = WorkflowUtils.buildStringFilter(app, properties.getUsername()); + passwordFilter = WorkflowUtils.buildStringFilter(app, properties.getPassword()); + } else if (authPolicy.getDigest().getDigestAuthenticationPolicySecret() != null) { + String secretName = + checkSecret(workflow, authPolicy.getDigest().getDigestAuthenticationPolicySecret()); + userFilter = (w, t, m) -> secretProp(w, secretName, USER); + passwordFilter = (w, t, m) -> secretProp(w, secretName, PASSWORD); + } else { + throw new IllegalStateException( + "Both secret and properties are null for digest authorization"); + } + this.method = method; + } + + @Override + public String scheme() { + return DIGEST; + } + + @Override + public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { + try { + HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + connection.setRequestMethod(method); + int responseCode = connection.getResponseCode(); + if (responseCode == 401) { + DigestServerInfo serverInfo = + DigestServerInfo.from(connection.getHeaderField("WWW-Authenticate")); + String userName = userFilter.apply(workflow, task, model); + String path = uri.getPath(); + String nonceCount; + String clientNonce; + if (serverInfo.qop.isPresent() || serverInfo.algorithm == Algorithm.MD5SESSS) { + nonceCount = String.format("%08x", nc.getAndIncrement()); + clientNonce = AuthUtils.getRandomHexString(); + } else { + nonceCount = null; + clientNonce = null; + } + final String hash1 = + calculateHash(userName, serverInfo.realm, passwordFilter.apply(workflow, task, model)); + final String ha1 = + serverInfo.algorithm == Algorithm.MD5SESSS + ? calculateHash(hash1, serverInfo.nonce, clientNonce) + : hash1; + final String ha2 = calculateHash(method, path); + String response = + serverInfo + .qop + .map( + qop -> + calculateHash( + ha1, + serverInfo.nonce, + nonceCount, + clientNonce, + qop.toString().toLowerCase(), + ha2)) + .orElseGet(() -> calculateHash(ha1, serverInfo.nonce, ha2)); + + return buildResponseInfo(serverInfo, userName, path, clientNonce, nonceCount, response); + } else { + throw new IllegalStateException( + "URI " + + uri + + " is not digest protected, it returned code " + + responseCode + + " when invoked without authentication header, but it should have returned 401 as per RFC 2617"); + } + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + + private String buildResponseInfo( + DigestServerInfo digestInfo, + String userName, + String uri, + String clientNonce, + String nonceCount, + String response) { + StringBuilder sb = new StringBuilder("username=\"" + userName + "\""); + addHeader(sb, REALM, digestInfo.realm); + addHeader(sb, NONCE, digestInfo.nonce); + addHeader(sb, "uri", uri); + digestInfo.qop.ifPresent(qop -> addUnquotedHeader(sb, QOP_KEY, qop.toString().toLowerCase())); + if (clientNonce != null) { + addUnquotedHeader(sb, "nc", nonceCount); + addHeader(sb, "cnonce", clientNonce); + } + addHeader(sb, "response", response); + if (digestInfo.opaque != null) { + addHeader(sb, OPAQUE, digestInfo.opaque); + } + return sb.toString(); + } + + private StringBuilder addHeader(StringBuilder sb, String key, String value) { + return sb.append(',').append(key).append('=').append('"').append(value).append('"'); + } + + private StringBuilder addUnquotedHeader(StringBuilder sb, String key, String value) { + return sb.append(',').append(key).append('=').append(value); + } + + private String calculateHash(String firstOne, String... strs) { + try { + StringBuilder sb = new StringBuilder(firstOne); + for (String str : strs) { + sb.append(':').append(str); + } + return printHexBinary(MessageDigest.getInstance("MD5").digest(sb.toString().getBytes())); + } catch (NoSuchAlgorithmException ex) { + throw new UnsupportedOperationException("System is not supporting MD5!!!!", ex); + } + } + + private static final char[] hexCode = "0123456789abcdef".toCharArray(); + + private static String printHexBinary(byte[] data) { + StringBuilder sb = new StringBuilder(data.length * 2); + for (byte b : data) { + sb.append(hexCode[(b >> 4) & 0xF]); + sb.append(hexCode[(b & 0xF)]); + } + return sb.toString(); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java index e3854e12..a9a91449 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java @@ -99,20 +99,21 @@ public T load( WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel model) { - return loadURI( + URI uri = uriSupplier(endPoint) .apply( workflowContext, taskContext, - model == null ? application.modelFactory().fromNull() : model), + model == null ? application.modelFactory().fromNull() : model); + return loadURI( + uri, function, AuthProviderFactory.getAuth( workflowContext.definition(), endPoint.getEndpointConfiguration()) .map( auth -> AuthUtils.authHeaderValue( - auth.authScheme(), - auth.authParameter(workflowContext, taskContext, model)))); + auth.scheme(), auth.content(workflowContext, taskContext, model, uri)))); } public T loadURI(URI uri, Function function) { diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java similarity index 68% rename from impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java rename to impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java index cbcbc867..36ecb52e 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java @@ -23,23 +23,31 @@ import io.serverlessworkflow.impl.WorkflowError; import io.serverlessworkflow.impl.WorkflowException; import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.auth.AuthProvider; +import io.serverlessworkflow.impl.auth.AuthUtils; import jakarta.ws.rs.client.Invocation.Builder; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status.Family; +import java.net.URI; +import java.util.Optional; -abstract class AbstractRequestSupplier implements RequestSupplier { +abstract class AbstractRequestExecutor implements RequestExecutor { private final boolean redirect; + private final Optional authProvider; + protected final String method; - public AbstractRequestSupplier(boolean redirect) { + public AbstractRequestExecutor(String method, boolean redirect, Optional auth) { this.redirect = redirect; + this.method = method; + this.authProvider = auth; } @Override public WorkflowModel apply( - Builder request, WorkflowContext workflow, TaskContext task, WorkflowModel model) { + Builder request, URI uri, WorkflowContext workflow, TaskContext task, WorkflowModel model) { HttpModelConverter converter = HttpConverterResolver.converter(workflow, task); - + authProvider.ifPresent(auth -> addAuthHeader(auth, uri, request, workflow, task, model)); Response response = invokeRequest(request, converter, workflow, task, model); validateStatus(task, response, converter); return workflow @@ -51,7 +59,6 @@ public WorkflowModel apply( private void validateStatus(TaskContext task, Response response, HttpModelConverter converter) { Family statusFamily = response.getStatusInfo().getFamily(); - if (statusFamily != SUCCESSFUL && (!this.redirect || statusFamily != REDIRECTION)) { throw new WorkflowException( converter @@ -66,4 +73,17 @@ protected abstract Response invokeRequest( WorkflowContext workflow, TaskContext task, WorkflowModel model); + + private void addAuthHeader( + AuthProvider auth, + URI uri, + Builder request, + WorkflowContext workflow, + TaskContext task, + WorkflowModel model) { + String scheme = auth.scheme(); + String parameter = auth.content(workflow, task, model, uri); + task.authorization(scheme, parameter); + request.header(AuthUtils.AUTH_HEADER_NAME, AuthUtils.authHeaderValue(scheme, parameter)); + } } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/CallableTaskHttpExecutorBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/CallableTaskHttpExecutorBuilder.java index 848bfe93..a5b3eb04 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/CallableTaskHttpExecutorBuilder.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/CallableTaskHttpExecutorBuilder.java @@ -23,7 +23,6 @@ import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.impl.WorkflowDefinition; import io.serverlessworkflow.impl.WorkflowMutablePosition; -import io.serverlessworkflow.impl.WorkflowUtils; import io.serverlessworkflow.impl.WorkflowValueResolver; import io.serverlessworkflow.impl.executors.CallableTask; import io.serverlessworkflow.impl.executors.CallableTaskBuilder; @@ -70,8 +69,6 @@ public void init(CallHTTP task, WorkflowDefinition definition, WorkflowMutablePo builder.withBody(httpArgs.getBody()); builder.withMethod(httpArgs.getMethod().toUpperCase()); builder.redirect(httpArgs.isRedirect()); - builder.timeout( - WorkflowUtils.getTaskTimeout(definition.application(), definition.workflow(), task)); } @Override diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpClientResolver.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpClientResolver.java index e161008d..fc40b257 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpClientResolver.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpClientResolver.java @@ -20,8 +20,6 @@ import io.serverlessworkflow.impl.WorkflowContext; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; -import java.time.Duration; -import java.util.Optional; public class HttpClientResolver { @@ -32,14 +30,6 @@ private static class DefaultHolder { } public static Client client(WorkflowContext workflowContext, TaskContext taskContext) { - return client(workflowContext, taskContext, false, Optional.empty()); - } - - public static Client client( - WorkflowContext workflowContext, - TaskContext taskContext, - boolean redirect, - Optional timeout) { WorkflowApplication appl = workflowContext.definition().application(); return appl.additionalObject(HTTP_CLIENT_PROVIDER, workflowContext, taskContext) .orElseGet(() -> DefaultHolder.client); diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java index 825a61f7..7ca9230e 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java @@ -18,82 +18,59 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowUtils; import io.serverlessworkflow.impl.WorkflowValueResolver; -import io.serverlessworkflow.impl.auth.AuthProvider; -import io.serverlessworkflow.impl.auth.AuthUtils; import io.serverlessworkflow.impl.executors.CallableTask; import jakarta.ws.rs.client.Invocation.Builder; import jakarta.ws.rs.client.WebTarget; +import java.net.URI; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; public class HttpExecutor implements CallableTask { - private final WorkflowValueResolver targetSupplier; + private final WorkflowValueResolver uriSupplier; + private final Optional> pathSupplier; private final Optional>> headersMap; private final Optional>> queryMap; - private final Optional authProvider; - private final RequestSupplier requestFunction; + private final RequestExecutor requestFunction; HttpExecutor( - WorkflowValueResolver targetSupplier, + WorkflowValueResolver uriSupplier, Optional>> headersMap, Optional>> queryMap, - Optional authProvider, - RequestSupplier requestFunction) { - this.targetSupplier = targetSupplier; + RequestExecutor requestFunction, + Optional> pathSupplier) { + this.uriSupplier = uriSupplier; this.headersMap = headersMap; this.queryMap = queryMap; - this.authProvider = authProvider; this.requestFunction = requestFunction; - } - - private static class TargetQuerySupplier implements Supplier { - - private WebTarget target; - - public TargetQuerySupplier(WebTarget original) { - this.target = original; - } - - public void addQuery(String key, Object value) { - target = target.queryParam(key, value); - } - - public WebTarget get() { - return target; - } + this.pathSupplier = pathSupplier; } public CompletableFuture apply( WorkflowContext workflow, TaskContext taskContext, WorkflowModel input) { - TargetQuerySupplier supplier = - new TargetQuerySupplier(targetSupplier.apply(workflow, taskContext, input)); - queryMap.ifPresent( - q -> q.apply(workflow, taskContext, input).forEach((k, v) -> supplier.addQuery(k, v))); - Builder request = supplier.get().request(); + URI uri = + pathSupplier + .map( + p -> + WorkflowUtils.concatURI( + uriSupplier.apply(workflow, taskContext, input), + p.apply(workflow, taskContext, input))) + .orElse(uriSupplier.apply(workflow, taskContext, input)); + + WebTarget target = HttpClientResolver.client(workflow, taskContext).target(uri); + for (Entry entry : + queryMap.map(q -> q.apply(workflow, taskContext, input)).orElse(Map.of()).entrySet()) { + target = target.queryParam(entry.getKey(), entry.getValue()); + } + Builder request = target.request(); headersMap.ifPresent( h -> h.apply(workflow, taskContext, input).forEach((k, v) -> request.header(k, v))); return CompletableFuture.supplyAsync( - () -> { - authProvider.ifPresent( - auth -> addAuthHeader(auth, request, workflow, taskContext, input)); - return requestFunction.apply(request, workflow, taskContext, input); - }, + () -> requestFunction.apply(request, uri, workflow, taskContext, input), workflow.definition().application().executorService()); } - - private void addAuthHeader( - AuthProvider auth, - Builder request, - WorkflowContext workflow, - TaskContext task, - WorkflowModel model) { - String scheme = auth.authScheme(); - String parameter = auth.authParameter(workflow, task, model); - task.authorization(scheme, parameter); - request.header(AuthUtils.AUTH_HEADER_NAME, AuthUtils.authHeaderValue(scheme, parameter)); - } } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java index 05b5898a..92349d84 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java @@ -19,13 +19,9 @@ import io.serverlessworkflow.impl.WorkflowDefinition; import io.serverlessworkflow.impl.WorkflowUtils; import io.serverlessworkflow.impl.WorkflowValueResolver; -import io.serverlessworkflow.impl.auth.AuthProvider; import io.serverlessworkflow.impl.auth.AuthProviderFactory; import jakarta.ws.rs.HttpMethod; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.client.WebTarget; import java.net.URI; -import java.time.Duration; import java.util.Map; import java.util.Optional; @@ -35,18 +31,17 @@ public class HttpExecutorBuilder { private WorkflowValueResolver pathSupplier; private Object body; private String method = HttpMethod.GET; + private ReferenceableAuthenticationPolicy policy; private boolean redirect; - private Optional> timeout = Optional.empty(); private WorkflowValueResolver> headersMap; private WorkflowValueResolver> queryMap; - private Optional authProvider = Optional.empty(); private HttpExecutorBuilder(WorkflowDefinition definition) { this.definition = definition; } public HttpExecutorBuilder withAuth(ReferenceableAuthenticationPolicy policy) { - this.authProvider = AuthProviderFactory.getAuth(definition, policy); + this.policy = policy; return this; } @@ -88,69 +83,42 @@ public HttpExecutorBuilder redirect(boolean redirect) { return this; } - public HttpExecutorBuilder timeout(Optional> timeout) { - this.timeout = timeout; - return this; - } - public HttpExecutor build(String uri) { return build((w, f, n) -> URI.create(uri)); } public HttpExecutor build(WorkflowValueResolver uriSupplier) { - return new HttpExecutor( - getTargetSupplier(uriSupplier), + uriSupplier, Optional.ofNullable(headersMap), Optional.ofNullable(queryMap), - authProvider, - buildRequestSupplier()); - } - - private WorkflowValueResolver getTargetSupplier( - WorkflowValueResolver uriSupplier) { - return pathSupplier == null - ? (w, t, n) -> - HttpClientResolver.client(w, t, redirect, timeout.map(v -> v.apply(w, t, n))) - .target(uriSupplier.apply(w, t, n)) - : (w, t, n) -> - HttpClientResolver.client(w, t, redirect, timeout.map(v -> v.apply(w, t, n))) - .target( - WorkflowUtils.concatURI( - uriSupplier.apply(w, t, n), pathSupplier.apply(w, t, n))); + buildRequestExecutor(), + Optional.ofNullable(pathSupplier)); } public static HttpExecutorBuilder builder(WorkflowDefinition definition) { return new HttpExecutorBuilder(definition); } - private RequestSupplier buildRequestSupplier() { - switch (method.toUpperCase()) { + private RequestExecutor buildRequestExecutor() { + String theMethod = method.toUpperCase(); + switch (theMethod) { case HttpMethod.POST: - return new WithBodyRequestSupplier( - Invocation.Builder::post, definition.application(), body, redirect); case HttpMethod.PUT: - return new WithBodyRequestSupplier( - Invocation.Builder::put, definition.application(), body, redirect); - case HttpMethod.DELETE: - return new WithoutBodyRequestSupplier( - Invocation.Builder::delete, definition.application(), redirect); - case HttpMethod.HEAD: - return new WithoutBodyRequestSupplier( - Invocation.Builder::head, definition.application(), redirect); case HttpMethod.PATCH: - return new WithBodyRequestSupplier( - (request, entity) -> request.method("PATCH", entity), + return new WithBodyRequestExecutor( + theMethod, + redirect, + AuthProviderFactory.getAuth(definition, policy, method), definition.application(), - body, - redirect); + body); + case HttpMethod.DELETE: + case HttpMethod.HEAD: case HttpMethod.OPTIONS: - return new WithoutBodyRequestSupplier( - Invocation.Builder::options, definition.application(), redirect); case HttpMethod.GET: default: - return new WithoutBodyRequestSupplier( - Invocation.Builder::get, definition.application(), redirect); + return new WithoutBodyRequestExecutor( + theMethod, redirect, AuthProviderFactory.getAuth(definition, policy, method)); } } } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestExecutor.java similarity index 86% rename from impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestSupplier.java rename to impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestExecutor.java index 50155fff..02deedd6 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestSupplier.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestExecutor.java @@ -19,9 +19,10 @@ import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; import jakarta.ws.rs.client.Invocation.Builder; +import java.net.URI; @FunctionalInterface -interface RequestSupplier { +interface RequestExecutor { WorkflowModel apply( - Builder request, WorkflowContext workflow, TaskContext task, WorkflowModel model); + Builder request, URI uri, WorkflowContext workflow, TaskContext task, WorkflowModel model); } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestExecutor.java similarity index 72% rename from impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestSupplier.java rename to impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestExecutor.java index 949abf79..91661e29 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestSupplier.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestExecutor.java @@ -21,22 +21,21 @@ import io.serverlessworkflow.impl.WorkflowFilter; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowUtils; -import jakarta.ws.rs.client.Entity; +import io.serverlessworkflow.impl.auth.AuthProvider; import jakarta.ws.rs.client.Invocation.Builder; import jakarta.ws.rs.core.Response; -import java.util.function.BiFunction; +import java.util.Optional; -class WithBodyRequestSupplier extends AbstractRequestSupplier { +class WithBodyRequestExecutor extends AbstractRequestExecutor { private final WorkflowFilter bodyFilter; - private final BiFunction, Response> requestFunction; - public WithBodyRequestSupplier( - BiFunction, Response> requestFunction, + public WithBodyRequestExecutor( + String method, + boolean redirect, + Optional auth, WorkflowApplication application, - Object body, - boolean redirect) { - super(redirect); - this.requestFunction = requestFunction; + Object body) { + super(method, redirect, auth); bodyFilter = WorkflowUtils.buildWorkflowFilter(application, body); } @@ -47,7 +46,6 @@ protected Response invokeRequest( WorkflowContext workflow, TaskContext task, WorkflowModel model) { - return requestFunction.apply( - request, converter.toEntity(bodyFilter.apply(workflow, task, model))); + return request.method(method, converter.toEntity(bodyFilter.apply(workflow, task, model))); } } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestExecutor.java similarity index 70% rename from impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestSupplier.java rename to impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestExecutor.java index a9604005..5901a39d 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestSupplier.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestExecutor.java @@ -16,22 +16,17 @@ package io.serverlessworkflow.impl.executors.http; import io.serverlessworkflow.impl.TaskContext; -import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.auth.AuthProvider; import jakarta.ws.rs.client.Invocation.Builder; import jakarta.ws.rs.core.Response; -import java.util.function.Function; +import java.util.Optional; -class WithoutBodyRequestSupplier extends AbstractRequestSupplier { - private final Function requestFunction; +class WithoutBodyRequestExecutor extends AbstractRequestExecutor { - public WithoutBodyRequestSupplier( - Function requestFunction, - WorkflowApplication application, - boolean redirect) { - super(redirect); - this.requestFunction = requestFunction; + public WithoutBodyRequestExecutor(String method, boolean redirect, Optional auth) { + super(method, redirect, auth); } @Override @@ -41,6 +36,6 @@ protected Response invokeRequest( WorkflowContext workflow, TaskContext task, WorkflowModel model) { - return requestFunction.apply(request); + return request.method(method); } } diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/BasicAuthHttpTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/BasicAuthHttpTest.java index d5dacc3d..d0d627e8 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/BasicAuthHttpTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/BasicAuthHttpTest.java @@ -79,7 +79,7 @@ void close() throws IOException { }) void testBasic(String path) throws IOException { Workflow workflow = readWorkflowFromClasspath(path); - WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of("petId", 1)); + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); instance.start().join(); assertThat(instance.context()).isNotNull(); Map authInfo = diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/DigestAuthHttpTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/DigestAuthHttpTest.java new file mode 100644 index 00000000..0ed1562c --- /dev/null +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/DigestAuthHttpTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl.test; + +import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mockStatic; + +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.ExecutorServiceFactory; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowInstance; +import io.serverlessworkflow.impl.auth.AuthUtils; +import io.serverlessworkflow.impl.jackson.JsonUtils; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +public class DigestAuthHttpTest { + private static WorkflowApplication app; + private MockWebServer apiServer; + + @BeforeAll + static void init() { + app = + WorkflowApplication.builder() + .withExecutorFactory( + new ExecutorServiceFactory() { + private ExecutorService service = + Executors.newFixedThreadPool( + 1, + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread( + new Runnable() { + @Override + public void run() { + try (MockedStatic mocked = + mockStatic(AuthUtils.class)) { + mocked + .when(AuthUtils::getRandomHexString) + .thenReturn("0a4f113b"); + r.run(); + } + } + }); + } + }); + + @Override + public void close() throws Exception { + service.shutdownNow(); + } + + @Override + public ExecutorService get() { + return service; + } + }) + .withSecretManager( + k -> + k.equals("mySecret") + ? Map.of("username", "Mufasa", "password", "Circle Of Life") + : Map.of()) + .build(); + } + + @AfterAll + static void cleanup() { + app.close(); + } + + @BeforeEach + void setup() throws IOException { + apiServer = new MockWebServer(); + apiServer.start(10110); + apiServer.enqueue( + new MockResponse() + .setResponseCode(401) + .setHeader( + "WWW-Authenticate", + "Digest realm=\"testrealm@host.com\",qop=\"auth,auth-int\",nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"")); + apiServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(JsonUtils.mapper().createObjectNode().toString())); + } + + @AfterEach + void close() throws IOException { + apiServer.close(); + } + + @Test + void testDigest() throws IOException { + Workflow workflow = readWorkflowFromClasspath("workflows-samples/digest-properties-auth.yaml"); + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); + instance.start().join(); + assertThat(instance.context()).isNotNull(); + Map authInfo = + (Map) instance.context().asMap().orElseThrow().get("info"); + + assertThat(authInfo.get("scheme")).isEqualTo("Digest"); + assertThat(((String) authInfo.get("parameter"))) + .isEqualTo( + "username=\"Mufasa\",realm=\"testrealm@host.com\",nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",uri=\"/dir/index.html\",qop=auth,nc=00000001," + + "cnonce=\"0a4f113b\"," + + "response=\"6629fae49393a05397450978507c4ef1\"," + + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""); + } +} diff --git a/impl/test/src/test/resources/workflows-samples/digest-properties-auth.yaml b/impl/test/src/test/resources/workflows-samples/digest-properties-auth.yaml new file mode 100644 index 00000000..52af5092 --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/digest-properties-auth.yaml @@ -0,0 +1,24 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: digest-properties-auth + version: 1.0.0 +use: + secrets: + - mySecret +do: + - getPet: + call: http + with: + headers: + content-type: application/json + method: get + endpoint: + uri: http://localhost:10110/dir/index.html + authentication: + digest: + username: ${$secret.mySecret.username} + password: ${$secret.mySecret.password} + export: + as: + info: ${$authorization}