diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AccessTokenProvider.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AccessTokenProvider.java index 6ff1a4b81..55f928da4 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AccessTokenProvider.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AccessTokenProvider.java @@ -18,7 +18,9 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; +import java.util.concurrent.CompletableFuture; public interface AccessTokenProvider { - JWT validateAndGet(WorkflowContext workflow, TaskContext context, WorkflowModel model); + CompletableFuture validateAndGet( + WorkflowContext workflow, TaskContext context, WorkflowModel model); } 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 7dffc6295..2ff043471 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 @@ -19,10 +19,12 @@ import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; import java.net.URI; +import java.util.concurrent.CompletableFuture; public interface AuthProvider { String scheme(); - String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri); + CompletableFuture content( + WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri); } 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 11694c74d..172931e2b 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 @@ -30,6 +30,7 @@ import io.serverlessworkflow.impl.WorkflowValueResolver; import java.net.URI; import java.util.Base64; +import java.util.concurrent.CompletableFuture; class BasicAuthProvider implements AuthProvider { @@ -58,15 +59,17 @@ public BasicAuthProvider( } @Override - public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { - return new String( - Base64.getEncoder() - .encode( - String.format( - USER_PASSWORD, - userFilter.apply(workflow, task, model), - passwordFilter.apply(workflow, task, model)) - .getBytes())); + public CompletableFuture content( + WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { + return CompletableFuture.completedFuture( + new String( + Base64.getEncoder() + .encode( + String.format( + USER_PASSWORD, + userFilter.apply(workflow, task, model), + passwordFilter.apply(workflow, task, model)) + .getBytes()))); } @Override 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 b445c8748..a0521dfc4 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 @@ -29,6 +29,7 @@ import io.serverlessworkflow.impl.WorkflowUtils; import io.serverlessworkflow.impl.WorkflowValueResolver; import java.net.URI; +import java.util.concurrent.CompletableFuture; class BearerAuthProvider implements AuthProvider { @@ -49,8 +50,9 @@ public BearerAuthProvider( } @Override - public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { - return tokenFilter.apply(workflow, task, model); + public CompletableFuture content( + WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { + return CompletableFuture.completedFuture(tokenFilter.apply(workflow, task, model)); } @Override 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 73c0e45f7..e1e8fb6e2 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 @@ -29,8 +29,9 @@ import java.net.URI; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.CompletableFuture; -abstract class CommonOAuthProvider implements AuthProvider { +public abstract class CommonOAuthProvider implements AuthProvider { private final WorkflowValueResolver tokenProvider; @@ -47,8 +48,12 @@ protected CommonOAuthProvider(WorkflowValueResolver tokenPr } @Override - public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { - return tokenProvider.apply(workflow, task, model).validateAndGet(workflow, task, model).token(); + public CompletableFuture content( + WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { + return tokenProvider + .apply(workflow, task, model) + .validateAndGet(workflow, task, model) + .thenApply(JWT::token); } @Override 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 index e990b91b0..614008c9b 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/DigestAuthProvider.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/DigestAuthProvider.java @@ -31,7 +31,6 @@ import io.serverlessworkflow.impl.WorkflowValueResolver; import io.serverlessworkflow.impl.utils.RandomFactory; import java.io.IOException; -import java.io.UncheckedIOException; import java.net.HttpURLConnection; import java.net.URI; import java.security.MessageDigest; @@ -39,6 +38,7 @@ import java.util.Optional; import java.util.Random; import java.util.StringTokenizer; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -146,7 +146,8 @@ public String scheme() { } @Override - public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { + public CompletableFuture content( + WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { try { HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); connection.setRequestMethod(method); @@ -186,17 +187,19 @@ public String content(WorkflowContext workflow, TaskContext task, WorkflowModel ha2)) .orElseGet(() -> calculateHash(ha1, serverInfo.nonce, ha2)); - return buildResponseInfo(serverInfo, userName, path, clientNonce, nonceCount, response); + return CompletableFuture.completedFuture( + 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"); + return CompletableFuture.failedFuture( + 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); + return CompletableFuture.failedFuture(io); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java index 78c679bff..cff3ee3ca 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java @@ -16,29 +16,79 @@ package io.serverlessworkflow.impl.auth; import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; -public interface JWT { +public record JWT(String token, Map header, Map claims) { - String token(); + public Optional expiresAt() { + return toInstant(claims.get("exp")); + } - List audience(); + public Optional issuedAt() { + return toInstant(claims.get("iat")); + } - Map claims(); + public Collection audience() { + return toCollection(claims.get("aud"), String.class); + } - Optional claim(String name, Class type); + public Optional issuer() { + return Optional.ofNullable((String) claims.get("iss")); + } - Optional expiresAt(); + public Optional subject() { + return Optional.ofNullable((String) claims.get("sub")); + } - Map header(); + public Optional type() { + return header.containsKey("typ") + ? Optional.of((String) header.get("typ")) + : Optional.ofNullable((String) claims.get("typ")); + } - Optional issuedAt(); + static Optional toInstant(Object v) { + if (v == null) { + return Optional.empty(); + } + if (v instanceof Instant i) { + return Optional.of(i); + } + if (v instanceof Number n) { + return Optional.of(Instant.ofEpochSecond((n.longValue()))); + } + if (v instanceof String s) { + try { + long sec = Long.parseLong(s.trim()); + return Optional.of(Instant.ofEpochSecond((sec))); + } catch (NumberFormatException ignored) { + try { + return Optional.of(Instant.parse(s.trim())); + } catch (DateTimeParseException ex) { + } + } + } + return Optional.empty(); + } - Optional issuer(); - - Optional subject(); - - Optional type(); + /* Does not support primitive types intentionally, as they are not used in that context. */ + static Collection toCollection(Object v, Class clazz) { + if (v == null) { + return List.of(); + } + if (clazz.isInstance(v)) { + return List.of(clazz.cast(v)); + } + if (v instanceof Collection col) { + return col; + } + if (v.getClass().isArray() && v.getClass().getComponentType().equals(clazz)) { + return Arrays.asList((T[]) v); + } + return List.of(); + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuth2AuthProvider.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuth2AuthProvider.java index 424fc9aca..6bce3d814 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuth2AuthProvider.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuth2AuthProvider.java @@ -18,7 +18,7 @@ import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.impl.WorkflowApplication; -class OAuth2AuthProvider extends CommonOAuthProvider { +public class OAuth2AuthProvider extends CommonOAuthProvider { public OAuth2AuthProvider( WorkflowApplication application, Workflow workflow, OAuthPolicyData policyData) { diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OpenIdAuthProvider.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OpenIdAuthProvider.java index 197b23c49..80dd4138b 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OpenIdAuthProvider.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OpenIdAuthProvider.java @@ -18,7 +18,7 @@ import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.impl.WorkflowApplication; -class OpenIdAuthProvider extends CommonOAuthProvider { +public class OpenIdAuthProvider extends CommonOAuthProvider { public OpenIdAuthProvider( WorkflowApplication application, Workflow workflow, OAuthPolicyData policyData) { diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java index c6003cc0e..b7bb1432b 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java @@ -56,32 +56,31 @@ public RunScriptExecutor( @Override public CompletableFuture apply( WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { - ScriptContext scriptContext = - new ScriptContext( - argumentExpr.map(m -> m.apply(workflowContext, taskContext, input)).orElse(Map.of()), - environmentExpr.map(m -> m.apply(workflowContext, taskContext, input)).orElse(Map.of()), - codeSupplier.apply(workflowContext, taskContext, input), - returnType); if (isAwait) { return CompletableFuture.supplyAsync( - () -> runScript(scriptContext, workflowContext, taskContext, input), + () -> runScript(workflowContext, taskContext, input), workflowContext.definition().application().executorService()); } else { workflowContext .definition() .application() .executorService() - .submit(() -> runScript(scriptContext, workflowContext, taskContext, input)); + .submit(() -> runScript(workflowContext, taskContext, input)); return CompletableFuture.completedFuture(input); } } private WorkflowModel runScript( - ScriptContext scriptContext, - WorkflowContext workflowContext, - TaskContext taskContext, - WorkflowModel input) { + WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { try { + ScriptContext scriptContext = + new ScriptContext( + argumentExpr.map(m -> m.apply(workflowContext, taskContext, input)).orElse(Map.of()), + environmentExpr + .map(m -> m.apply(workflowContext, taskContext, input)) + .orElse(Map.of()), + codeSupplier.apply(workflowContext, taskContext, input), + returnType); return taskRunner.runScript(scriptContext, workflowContext, taskContext, input); } catch (Exception ex) { throw new WorkflowException(WorkflowError.runtime(taskContext, ex).build()); 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 08e3d0725..18db10991 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 @@ -113,7 +113,8 @@ public T load( .map( auth -> AuthUtils.authHeaderValue( - auth.scheme(), auth.content(workflowContext, taskContext, model, uri)))); + auth.scheme(), + auth.content(workflowContext, taskContext, model, uri).join()))); } public T loadURI(URI uri, Function function) { diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/auth/JWTTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/auth/JWTTest.java new file mode 100644 index 000000000..b6661fc9e --- /dev/null +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/auth/JWTTest.java @@ -0,0 +1,37 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +public class JWTTest { + + @Test + public void testToStringCollection() { + String[] test = {"javierito", "fulanito", "menganito"}; + assertThat(JWT.toCollection(test, String.class)) + .isEqualTo(List.of("javierito", "fulanito", "menganito")); + } + + @Test + public void testToIntCollection() { + Integer[] test = {1, 2, 3}; + assertThat(JWT.toCollection(test, Integer.class)).isEqualTo(List.of(1, 2, 3)); + } +} diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java index 4fd11b188..ab67b4cac 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java @@ -32,6 +32,8 @@ import jakarta.ws.rs.core.Response.Status.Family; import java.net.URI; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; abstract class AbstractRequestExecutor implements RequestExecutor { @@ -46,10 +48,30 @@ public AbstractRequestExecutor(String method, boolean redirect, Optional apply( 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)); + ExecutorService executorService = workflow.definition().application().executorService(); + CompletableFuture converterFuture = + CompletableFuture.supplyAsync( + () -> HttpConverterResolver.converter(workflow, task), executorService); + CompletableFuture authFuture = + authProvider + .map( + auth -> + converterFuture.thenCompose( + converter -> + addAuthHeader(auth, uri, request, workflow, task, model) + .thenApply(v -> converter))) + .orElse(converterFuture); + return authFuture.thenApply(converter -> doRequest(request, converter, workflow, task, model)); + } + + private WorkflowModel doRequest( + Builder request, + HttpModelConverter converter, + WorkflowContext workflow, + TaskContext task, + WorkflowModel model) { try (Response response = invokeRequest(request, converter, workflow, task, model)) { validateStatus(task, response, converter); return workflow @@ -84,7 +106,7 @@ protected abstract Response invokeRequest( TaskContext task, WorkflowModel model); - private void addAuthHeader( + private CompletableFuture addAuthHeader( AuthProvider auth, URI uri, Builder request, @@ -92,8 +114,12 @@ private void addAuthHeader( 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)); + return auth.content(workflow, task, model, uri) + .thenAccept( + parameter -> { + 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/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java index 39f9e0911..31edeb8f3 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 @@ -73,8 +73,6 @@ public CompletableFuture apply( Builder request = target.request(); requestDecorators.forEach(d -> d.decorate(request, workflow, taskContext)); headersMap.ifPresent(h -> h.apply(workflow, taskContext, input).forEach(request::header)); - return CompletableFuture.supplyAsync( - () -> requestFunction.apply(request, uri, workflow, taskContext, input), - workflow.definition().application().executorService()); + return requestFunction.apply(request, uri, workflow, taskContext, input); } } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestExecutor.java index 02deedd6e..a5caf54e6 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestExecutor.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestExecutor.java @@ -20,9 +20,10 @@ import io.serverlessworkflow.impl.WorkflowModel; import jakarta.ws.rs.client.Invocation.Builder; import java.net.URI; +import java.util.concurrent.CompletableFuture; @FunctionalInterface interface RequestExecutor { - WorkflowModel apply( + CompletableFuture apply( Builder request, URI uri, WorkflowContext workflow, TaskContext task, WorkflowModel model); } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java index e13f5a744..330ebb23d 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java @@ -40,6 +40,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; class JaxRSAccessTokenProvider implements AccessTokenProvider { @@ -54,7 +55,8 @@ class JaxRSAccessTokenProvider implements AccessTokenProvider { this.jwtConverter = converter; } - public JWT validateAndGet(WorkflowContext workflow, TaskContext context, WorkflowModel model) { + public CompletableFuture validateAndGet( + WorkflowContext workflow, TaskContext context, WorkflowModel model) { Map token = invoke(workflow, context, model); JWT jwt = jwtConverter.fromToken((String) token.get("access_token")); if (issuers != null && !issuers.isEmpty()) { @@ -66,7 +68,7 @@ public JWT validateAndGet(WorkflowContext workflow, TaskContext context, Workflo } }); } - return jwt; + return CompletableFuture.completedFuture(jwt); } private Map invoke( diff --git a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTConverter.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTConverter.java index f1e608f2f..ec304960f 100644 --- a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTConverter.java +++ b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTConverter.java @@ -36,7 +36,7 @@ public JWT fromToken(String token) throws IllegalArgumentException { throw new IllegalArgumentException( "Invalid JWT token format. There should at least two parts separated by :"); } - return new JacksonJWTImpl(token, fromPart2Map(parts[0]), fromPart2Map(parts[1])); + return new JWT(token, fromPart2Map(parts[0]), fromPart2Map(parts[1])); } private static final Map fromPart2Map(String part) { diff --git a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTImpl.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTImpl.java deleted file mode 100644 index 88fb7f762..000000000 --- a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTImpl.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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.executors.http.oauth.jackson; - -import io.serverlessworkflow.impl.auth.JWT; -import java.time.Instant; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -public class JacksonJWTImpl implements JWT { - - private final Map header; - private final Map claims; - - private final String token; - - JacksonJWTImpl(String token, Map header, Map claims) { - this.token = Objects.requireNonNull(token, "token"); - this.header = Collections.unmodifiableMap(Objects.requireNonNull(header, "header")); - this.claims = Collections.unmodifiableMap(Objects.requireNonNull(claims, "claims")); - } - - @Override - public String token() { - return token; - } - - @Override - public Map header() { - return header; - } - - @Override - public Map claims() { - return claims; - } - - @Override - public Optional claim(String name, Class type) { - if (name == null || type == null) return Optional.empty(); - Object value = claims.get(name); - return type.isInstance(value) ? Optional.of(type.cast(value)) : Optional.empty(); - } - - @Override - public Optional issuer() { - return Optional.ofNullable(asString(claims.get("iss"))); - } - - @Override - public Optional subject() { - return Optional.ofNullable(asString(claims.get("sub"))); - } - - @Override - public List audience() { - Object aud = claims.get("aud"); - if (aud == null) { - return List.of(); - } else if (aud instanceof String asString) { - return List.of(asString); - } else if (aud instanceof String[] asStringArray) { - return Arrays.asList(asStringArray); - } - return ((Collection) aud).stream().map(String::valueOf).toList(); - } - - @Override - public Optional issuedAt() { - return Optional.ofNullable(toInstant(claims.get("iat"))); - } - - @Override - public Optional expiresAt() { - return Optional.ofNullable(toInstant(claims.get("exp"))); - } - - @Override - public Optional type() { - return header.containsKey("typ") - ? Optional.of(asString(header.get("typ"))) - : Optional.ofNullable(asString(claims.get("typ"))); - } - - private static Instant toInstant(Object v) { - if (v == null) { - return null; - } - if (v instanceof Instant i) { - return i; - } - if (v instanceof Number n) { - return Instant.ofEpochSecond(n.longValue()); - } - if (v instanceof String s) { - try { - long sec = Long.parseLong(s.trim()); - return Instant.ofEpochSecond(sec); - } catch (NumberFormatException ignored) { - try { - return Instant.parse(s.trim()); - } catch (Exception e) { - throw new IllegalArgumentException("Cannot parse time claim: " + s, e); - } - } - } - throw new IllegalArgumentException("Unsupported time claim type: " + v.getClass()); - } - - private static String asString(Object v) { - return v == null ? null : String.valueOf(v).trim(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof JacksonJWTImpl that)) return false; - return Objects.equals(token, that.token); - } - - @Override - public int hashCode() { - return Objects.hash(token); - } -} diff --git a/impl/jwt-impl/src/test/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImplTest.java b/impl/jwt-impl/src/test/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImplTest.java index bbd870d6f..c96d75767 100644 --- a/impl/jwt-impl/src/test/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImplTest.java +++ b/impl/jwt-impl/src/test/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImplTest.java @@ -16,6 +16,7 @@ package io.serverlessworkflow.impl.http.jwt; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -43,27 +44,23 @@ void tokenRoundTrip() { @Test void headerAndType() { JWT jwt = converter.fromToken(JWT_TOKEN); - Map h = jwt.header(); assertEquals("RS256", h.get("alg")); assertTrue(h.containsKey("kid")); assertEquals("JWT", h.get("typ")); - assertEquals(Optional.of("JWT"), jwt.type(), "type() must be a header.typ"); } @Test void payloadTypIsArbitraryClaim() { JWT jwt = converter.fromToken(JWT_TOKEN); - - assertEquals(Optional.of("Bearer"), jwt.claim("typ", String.class)); + assertEquals("Bearer", jwt.claims().get("typ")); assertEquals("JWT", jwt.header().get("typ")); } @Test void standardClaims() { JWT jwt = converter.fromToken(JWT_TOKEN); - assertEquals(Optional.of("http://localhost:8088/realms/test-realm"), jwt.issuer()); assertEquals(Optional.of("a1d7e1ee-5ff3-477b-a2da-3a46da8efdd1"), jwt.subject()); assertEquals(List.of("account"), jwt.audience()); @@ -72,7 +69,6 @@ void standardClaims() { @Test void timeClaims() { JWT jwt = converter.fromToken(JWT_TOKEN); - assertEquals(Instant.ofEpochSecond(1756148257L), jwt.expiresAt().orElseThrow()); assertEquals(Instant.ofEpochSecond(1756147957L), jwt.issuedAt().orElseThrow()); } @@ -80,19 +76,9 @@ void timeClaims() { @Test void typedClaimAccess() { JWT jwt = converter.fromToken(JWT_TOKEN); - - assertEquals(Optional.of("any@example.org"), jwt.claim("email", String.class)); - assertEquals(Optional.of(false), jwt.claim("email_verified", Boolean.class)); - assertTrue(jwt.claim("nonexistent", String.class).isEmpty()); - assertTrue(jwt.claim("email_verified", String.class).isEmpty()); - } - - @Test - void mapsAreUnmodifiable() { - JWT jwt = converter.fromToken(JWT_TOKEN); - - assertThrows(UnsupportedOperationException.class, () -> jwt.claims().put("x", 1)); - assertThrows(UnsupportedOperationException.class, () -> jwt.header().put("x", 1)); + assertEquals("any@example.org", jwt.claims().get("email")); + assertEquals(false, jwt.claims().get("email_verified")); + assertNull(jwt.claims().get("nonexistent")); } @Test diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java index 7cd10ff6e..56d63f1d5 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java @@ -56,38 +56,44 @@ class OpenAPIExecutor implements CallableTask { public CompletableFuture apply( WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { - // In the same workflow, access to an already cached document - final OperationDefinition operationDefinition = - processor.parse( - workflowContext - .definition() - .resourceLoader() - .load( - resource, - h -> readUnifiedOpenAPI(workflowContext, taskContext, h), - workflowContext, - taskContext, - input)); - - fillHttpBuilder(workflowContext.definition().application(), operationDefinition); - // One executor per operation, even if the document is the same - // Me may refactor this even further to reuse the same executor (since the base URI is the same, - // but the path differs, although some use cases may require different client configurations for - // different paths...) - Collection executors = - operationDefinition.getServers().stream().map(builder::build).toList(); + return CompletableFuture.supplyAsync( + () -> + processor.parse( + workflowContext + .definition() + .resourceLoader() + .load( + resource, + h -> readUnifiedOpenAPI(workflowContext, taskContext, h), + workflowContext, + taskContext, + input)), + workflowContext.definition().application().executorService()) + .thenCompose( + operationDefinition -> { + fillHttpBuilder(workflowContext.definition().application(), operationDefinition); + // One executor per operation, even if the document is the same + // Me may refactor this even further to reuse the same executor (since the base URI is + // the same, + // but the path differs, although some use cases may require different client + // configurations for + // different paths...) + Collection executors = + operationDefinition.getServers().stream().map(builder::build).toList(); - Iterator iter = executors.iterator(); - if (!iter.hasNext()) { - throw new IllegalArgumentException( - "List of servers is empty for schema " + resource.getName()); - } - CompletableFuture future = - iter.next().apply(workflowContext, taskContext, input); - while (iter.hasNext()) { - future.exceptionallyCompose(i -> iter.next().apply(workflowContext, taskContext, input)); - } - return future; + Iterator iter = executors.iterator(); + if (!iter.hasNext()) { + throw new IllegalArgumentException( + "List of servers is empty for schema " + resource.getName()); + } + CompletableFuture future = + iter.next().apply(workflowContext, taskContext, input); + while (iter.hasNext()) { + future.exceptionallyCompose( + i -> iter.next().apply(workflowContext, taskContext, input)); + } + return future; + }); } private void fillHttpBuilder(WorkflowApplication application, OperationDefinition operation) { diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/CustomAuthProviderFactoryOverrideTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/CustomAuthProviderFactoryOverrideTest.java index 46344a050..d454b954e 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/CustomAuthProviderFactoryOverrideTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/CustomAuthProviderFactoryOverrideTest.java @@ -33,6 +33,7 @@ import java.net.URI; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -98,9 +99,9 @@ public String scheme() { } @Override - public String content( + public CompletableFuture content( WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { - return frameworkToken; + return CompletableFuture.completedFuture(frameworkToken); } }); }