From 99fc8705e858d4bf288605eb8255e1c3b79cd9a1 Mon Sep 17 00:00:00 2001 From: Vic Seedoubleyew Date: Thu, 13 Jul 2023 19:11:32 +0200 Subject: [PATCH 1/2] Add ability to retrieve an activation's result Previously, client did not provide a convenient way to retrieve an activation's result. It was possible to use Swagger's generated code directly, but that would lead to duplication of the code already handled by OWskClient, and was not straightforward. This commit fixes it by adding a direct way, consistent with already existing functionality. --- .../org/projectodd/openwhisk/Activations.java | 52 +++++++++++++++++++ .../org/projectodd/openwhisk/OWskClient.java | 4 ++ 2 files changed, 56 insertions(+) create mode 100644 client/src/main/java/org/projectodd/openwhisk/Activations.java diff --git a/client/src/main/java/org/projectodd/openwhisk/Activations.java b/client/src/main/java/org/projectodd/openwhisk/Activations.java new file mode 100644 index 0000000..19c63fe --- /dev/null +++ b/client/src/main/java/org/projectodd/openwhisk/Activations.java @@ -0,0 +1,52 @@ +package org.projectodd.openwhisk; + +import org.projectodd.openwhisk.api.ActivationsApi; +import org.projectodd.openwhisk.invoker.ApiException; + +public class Activations { + + private final ActivationsApi activationsApi; + private final String namespace; + + Activations(OWskClient client) { + if (client == null) { + throw new IllegalArgumentException( + "Null passed as client to " + + Activations.class.getCanonicalName() + + " constructor"); + } + if (client.getClient() == null) { + throw new IllegalArgumentException( + "Null apiClient inside " + + OWskClient.class.getCanonicalName()); + } + if (client.getConfiguration() == null) { + throw new IllegalArgumentException( + "Null configuration inside " + + OWskClient.class.getCanonicalName()); + } + if (client.getConfiguration().getNamespace() == null + || client.getConfiguration().getNamespace() + .isEmpty()) { + throw new IllegalArgumentException( + "Missing namespace inside client configuration"); + } + this.activationsApi = new ActivationsApi(client.getClient()); + this.namespace = client.getConfiguration().getNamespace(); + } + + /** + * Get an activation's result + */ + public Object getActivationResult(String activationId) + throws ApiException { + if (activationId == null + || activationId.isEmpty()) { + throw new IllegalArgumentException( + "Missing activation id"); + } + return activationsApi + .namespacesNamespaceActivationsActivationidResultGet( + namespace, activationId).getResult(); + } +} diff --git a/client/src/main/java/org/projectodd/openwhisk/OWskClient.java b/client/src/main/java/org/projectodd/openwhisk/OWskClient.java index 8293823..2134678 100644 --- a/client/src/main/java/org/projectodd/openwhisk/OWskClient.java +++ b/client/src/main/java/org/projectodd/openwhisk/OWskClient.java @@ -42,4 +42,8 @@ ApiClient getClient() { public Actions actions() { return new Actions(this); } + + public Activations activations() { + return new Activations(this); + } } From bae682f250a2c97c0e3af55eb838b1fb8c28f10b Mon Sep 17 00:00:00 2001 From: Vic Seedoubleyew Date: Wed, 8 Nov 2023 00:16:05 +0100 Subject: [PATCH 2/2] Add ability to use OAuth to authenticate Previously, client only enabled to use the basic authentication mechanism when talking to the OpenWhisk server. This was not supported anymore by IBM Cloud Function. This commit fixes it by enabling to use OAuth. --- .../projectodd/openwhisk/Configuration.java | 73 +++++++++++++- .../org/projectodd/openwhisk/OWskClient.java | 20 +++- .../projectodd/openwhisk/OauthManager.java | 97 +++++++++++++++++++ 3 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 client/src/main/java/org/projectodd/openwhisk/OauthManager.java diff --git a/client/src/main/java/org/projectodd/openwhisk/Configuration.java b/client/src/main/java/org/projectodd/openwhisk/Configuration.java index a5a980e..ac783c6 100644 --- a/client/src/main/java/org/projectodd/openwhisk/Configuration.java +++ b/client/src/main/java/org/projectodd/openwhisk/Configuration.java @@ -3,6 +3,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.Properties; public class Configuration { @@ -20,6 +22,10 @@ public class Configuration { private String host = "localhost"; private int port = DEFAULT_PORT; private String auth; + private boolean useOauth; + private String oauthTokenUrl; + private Map oauthTokenRequestParameters; + private Map oauthTokenRequestHeaders; private String namespace = DEFAULT_NAMESPACE; private String actionPackage = ""; private boolean debugging = false; @@ -29,11 +35,19 @@ public class Configuration { private Configuration() { } - private Configuration(final String host, final int port, final String auth, final String namespace, final String actionPackage, + private Configuration(final String host, final int port, final String auth, final boolean useOauth, + final String oauthTokenUrl, + final Map oauthTokenRequestParameters, + final Map oauthTokenRequestHeaders, + final String namespace, final String actionPackage, boolean debugging, boolean insecure, final int timeout) { this.host = host; this.port = port; this.auth = auth; + this.useOauth = useOauth; + this.oauthTokenUrl = oauthTokenUrl; + this.oauthTokenRequestParameters = oauthTokenRequestParameters; + this.oauthTokenRequestHeaders = oauthTokenRequestHeaders; this.namespace = namespace; this.actionPackage = actionPackage; this.debugging = debugging; @@ -61,6 +75,22 @@ public String getAuth() { return auth; } + public boolean useOauth() { + return useOauth; + } + + public String getOauthTokenUrl() { + return oauthTokenUrl; + } + + public Map getOauthTokenRequestParameters() { + return new HashMap<>(oauthTokenRequestParameters); + } + + public Map getOauthTokenRequestHeaders() { + return new HashMap<>(oauthTokenRequestHeaders); + } + public String getNamespace() { return namespace; } @@ -113,6 +143,10 @@ public static class Builder { private int port = DEFAULT_PORT; private int timeout = DEFAULT_TIMEOUT; private String auth; + private boolean useOauth; + private String oauthTokenUrl; + private Map oauthTokenRequestParameters; + private Map oauthTokenRequestHeaders; private String namespace = DEFAULT_NAMESPACE; private String actionPackage = DEFAULT_ACTION_PACKAGE; @@ -123,6 +157,10 @@ public Builder(final Configuration configuration) { host = configuration.host; port = configuration.port; auth = configuration.auth; + useOauth = configuration.useOauth; + oauthTokenUrl = configuration.oauthTokenUrl; + oauthTokenRequestParameters = configuration.oauthTokenRequestParameters; + oauthTokenRequestHeaders = configuration.oauthTokenRequestHeaders; namespace = configuration.namespace; actionPackage = configuration.actionPackage; insecure = configuration.insecure; @@ -157,6 +195,30 @@ public Builder auth(String auth) { return this; } + public Builder useOauth(boolean useOauth) { + this.useOauth = useOauth; + return this; + } + + public Builder oauthTokenUrl(String oauthTokenUrl) { + this.oauthTokenUrl = oauthTokenUrl; + return this; + } + + public Builder oauthTokenRequestParameters( + Map requestParameters) { + this.oauthTokenRequestParameters = new HashMap<>( + requestParameters); + return this; + } + + public Builder oauthTokenRequestHeaders( + Map requestHeaders) { + this.oauthTokenRequestHeaders = new HashMap<>( + requestHeaders); + return this; + } + public Builder namespace(String namespace) { this.namespace = namespace; return this; @@ -168,7 +230,14 @@ public Builder actionPackage(String actionPackage) { } public Configuration build() { - return new Configuration(host, port, auth, namespace, actionPackage, debugging, insecure, timeout); + if (useOauth && (oauthTokenUrl == null + || oauthTokenUrl.isEmpty())) { + throw new IllegalStateException( + "No URL provided to request Oauth token"); + } + return new Configuration(host, port, auth, useOauth, oauthTokenUrl, + oauthTokenRequestParameters, oauthTokenRequestHeaders, + namespace, actionPackage, debugging, insecure, timeout); } } } diff --git a/client/src/main/java/org/projectodd/openwhisk/OWskClient.java b/client/src/main/java/org/projectodd/openwhisk/OWskClient.java index 2134678..7ed6f77 100644 --- a/client/src/main/java/org/projectodd/openwhisk/OWskClient.java +++ b/client/src/main/java/org/projectodd/openwhisk/OWskClient.java @@ -7,15 +7,17 @@ import static java.nio.charset.StandardCharsets.ISO_8859_1; import static okio.ByteString.encodeString; +import java.io.IOException; + public class OWskClient { private ApiClient client; private Configuration configuration; - public OWskClient() { + public OWskClient() throws IOException { configure(Configuration.load()); } - public void configure(final Configuration configuration) { + public void configure(final Configuration configuration) throws IOException { this.configuration = configuration; client = new ApiClient(); client.setBasePath(format("https://%s:%s/api/v1", configuration.getHost(), configuration.getPort())); @@ -25,7 +27,19 @@ public void configure(final Configuration configuration) { client.setVerifyingSsl(!configuration.isInsecure()); if(getConfiguration().getAuth()!= null) { - client.addDefaultHeader("Authorization", "Basic " + encodeString(getConfiguration().getAuth(), ISO_8859_1).base64()); + if (configuration.useOauth()) { + OauthManager oauthManager = new OauthManager( + configuration.getOauthTokenUrl(), + configuration.getOauthTokenRequestParameters(), + configuration.getOauthTokenRequestHeaders()); + client.getHttpClient().interceptors().add(oauthManager); + client.getHttpClient().setAuthenticator(oauthManager); + } else { + client.addDefaultHeader( + "Authorization", + "Basic " + encodeString(getConfiguration().getAuth(), ISO_8859_1) + .base64()); + } } client.setUserAgent("Incubating Apache OpenWhisk Java client"); client.setDebugging(configuration.isDebugging()); diff --git a/client/src/main/java/org/projectodd/openwhisk/OauthManager.java b/client/src/main/java/org/projectodd/openwhisk/OauthManager.java new file mode 100644 index 0000000..3985579 --- /dev/null +++ b/client/src/main/java/org/projectodd/openwhisk/OauthManager.java @@ -0,0 +1,97 @@ +package org.projectodd.openwhisk; + +import java.io.IOException; +import java.net.Proxy; +import java.util.Map; +import java.util.Map.Entry; + + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.squareup.okhttp.Authenticator; +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; + + +public class OauthManager implements Interceptor, Authenticator { + + private final OkHttpClient clientForToken = new OkHttpClient(); + private final JsonParser jsonParser = new JsonParser(); + private final String tokenUrl; + private final Map formParameters; + private final Map headers; + + private String token; + + public OauthManager(String tokenUrl, + Map formParameters, + Map headers) throws IOException { + this.tokenUrl = tokenUrl; + this.formParameters = formParameters; + this.headers = headers; + token = getNewToken(); + } + + private String getNewToken() throws IOException { + + StringBuilder bodyBuilder = new StringBuilder(); + for (Entry parameter : formParameters.entrySet()) { + if (bodyBuilder.length() > 0) { + bodyBuilder.append("&"); + } + bodyBuilder.append(parameter.getKey() + "=" + + parameter.getValue()); + } + Request.Builder requestBuilder = new Request.Builder() + .url(tokenUrl) + .post(RequestBody.create( + MediaType.parse("application/x-www-form-urlencoded"), + bodyBuilder.toString())); + + for (Entry header : headers.entrySet()) { + requestBuilder.header(header.getKey(), header.getValue()); + } + requestBuilder.header("Accept", "application/json"); + requestBuilder.header("Content-Type", + "application/x-www-form-urlencoded"); + Request request = requestBuilder.build(); + + Response response = clientForToken.newCall(request).execute(); + JsonObject parsedResponse = jsonParser.parse( + response.body().string()).getAsJsonObject(); + return parsedResponse.get("access_token").getAsString(); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request() + .newBuilder() + .addHeader("Authorization", "Bearer " + token) + .build(); + return chain.proceed(request); + } + + @Override + public Request authenticate(Proxy route, Response response) throws IOException { + if (!response.request().header("Authorization").equals(token)) { + return null; + } + token = getNewToken(); + if (token != null) { + return response.request().newBuilder() + .header("Authorization", "Bearer " + token) + .build(); + } else { + return null; + } + } + + @Override + public Request authenticateProxy(Proxy arg0, Response arg1) throws IOException { + return authenticate(arg0, arg1); + } +}