From 9b27f08ddd5b8ee71e66fde7c9e9fbc5c42b4a80 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Tue, 17 Apr 2018 13:00:44 -0400 Subject: [PATCH 01/26] feat(core): Add initial changes for IAM support --- .../service/WatsonService.java | 32 +++- .../service/security/CredentialOptions.java | 98 +++++++++++ .../service/security/IamToken.java | 55 ++++++ .../service/security/IamTokenManager.java | 161 ++++++++++++++++++ 4 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java create mode 100644 core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java create mode 100644 core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index 101de52ca92..bcd2ef98fff 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -33,6 +33,8 @@ import com.ibm.watson.developer_cloud.service.exception.TooManyRequestsException; import com.ibm.watson.developer_cloud.service.exception.UnauthorizedException; import com.ibm.watson.developer_cloud.service.exception.UnsupportedException; +import com.ibm.watson.developer_cloud.service.security.CredentialOptions; +import com.ibm.watson.developer_cloud.service.security.IamTokenManager; import com.ibm.watson.developer_cloud.util.CredentialUtils; import com.ibm.watson.developer_cloud.util.RequestUtils; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; @@ -72,6 +74,7 @@ public abstract class WatsonService { private String password; private String endPoint; private final String name; + private IamTokenManager tokenManager; private OkHttpClient client; @@ -280,13 +283,17 @@ public void setApiKey(String apiKey) { * @param builder the new authentication */ protected void setAuthentication(final Builder builder) { - if (getApiKey() == null) { + if (tokenManager != null) { + String accessToken = tokenManager.getToken(); + builder.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + } else if (getApiKey() == null) { if (skipAuthentication) { return; // chosen to skip authentication with the service } throw new IllegalArgumentException("apiKey or username and password were not specified"); + } else { + builder.addHeader(HttpHeaders.AUTHORIZATION, apiKey.startsWith(BASIC) ? apiKey : BASIC + apiKey); } - builder.addHeader(HttpHeaders.AUTHORIZATION, apiKey.startsWith(BASIC) ? apiKey : BASIC + apiKey); } /** @@ -325,6 +332,27 @@ public void setDefaultHeaders(final Map headers) { } } + protected void setCredentials(CredentialOptions credentialOptions) { + + // setting username and password + if (credentialOptions.getUsername() != null && credentialOptions.getPassword() != null) { + setUsernameAndPassword(credentialOptions.getUsername(), credentialOptions.getPassword()); + + // setting API key + } else if (credentialOptions.getApiKey() != null) { + setApiKey(credentialOptions.getApiKey()); + + // setting IAM token + } else if (credentialOptions.getAccessToken() != null || credentialOptions.getIamApiKey() != null) { + this.tokenManager = new IamTokenManager( + credentialOptions.getAccessToken(), + credentialOptions.getIamApiKey(), + credentialOptions.getRefreshToken(), + credentialOptions.getIamUrl() + ); + } + } + /* * (non-Javadoc) * diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java new file mode 100644 index 00000000000..3cffdf2ca20 --- /dev/null +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java @@ -0,0 +1,98 @@ +package com.ibm.watson.developer_cloud.service.security; + +public class CredentialOptions { + private String username; + private String password; + private String apiKey; + private String iamApiKey; + private String accessToken; + private String refreshToken; + private String iamUrl; + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getApiKey() { + return apiKey; + } + + public String getIamApiKey() { + return iamApiKey; + } + + public String getAccessToken() { + return accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public String getIamUrl() { + return iamUrl; + } + + public static class Builder { + private String username; + private String password; + private String apiKey; + private String iamApiKey; + private String accessToken; + private String refreshToken; + private String iamUrl; + + public CredentialOptions build() { + return new CredentialOptions(this); + } + + public Builder username(String username) { + this.username = username; + return this; + } + + public Builder password(String password) { + this.password = password; + return this; + } + + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public Builder iamApiKey(String iamApiKey) { + this.iamApiKey = iamApiKey; + return this; + } + + public Builder accessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public Builder refreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + public Builder iamUrl(String iamUrl) { + this.iamUrl = iamUrl; + return this; + } + } + + private CredentialOptions(Builder builder) { + this.username = builder.username; + this.password = builder.password; + this.apiKey = builder.apiKey; + this.iamApiKey = builder.iamApiKey; + this.accessToken = builder.accessToken; + this.refreshToken = builder.refreshToken; + this.iamUrl = builder.iamUrl; + } +} diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java new file mode 100644 index 00000000000..d185ccd194a --- /dev/null +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java @@ -0,0 +1,55 @@ +package com.ibm.watson.developer_cloud.service.security; + +import com.ibm.watson.developer_cloud.service.model.ObjectModel; + +class IamToken implements ObjectModel { + private String accessToken; + private String refreshToken; + private String tokenType; + private Long expiresIn; + private Long expiration; + + public static class Builder { + private String accessToken; + private String refreshToken; + + public IamToken build() { + return new IamToken(this); + } + + public Builder accessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public Builder refreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + } + + private IamToken(Builder builder) { + this.accessToken = builder.accessToken; + this.refreshToken = builder.refreshToken; + } + + public String getAccessToken() { + return accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public String getTokenType() { + return tokenType; + } + + public Long getExpiresIn() { + return expiresIn; + } + + public Long getExpiration() { + return expiration; + } +} diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java new file mode 100644 index 00000000000..2388a01f858 --- /dev/null +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -0,0 +1,161 @@ +package com.ibm.watson.developer_cloud.service.security; + +import com.google.gson.JsonObject; +import com.ibm.watson.developer_cloud.http.HttpClientSingleton; +import com.ibm.watson.developer_cloud.http.HttpMediaType; +import com.ibm.watson.developer_cloud.http.NameValue; +import com.ibm.watson.developer_cloud.http.RequestBuilder; +import com.ibm.watson.developer_cloud.http.Response; +import com.ibm.watson.developer_cloud.http.ResponseConverter; +import com.ibm.watson.developer_cloud.http.ServiceCall; +import com.ibm.watson.developer_cloud.http.ServiceCallback; +import com.ibm.watson.developer_cloud.http.ServiceCallbackWithDetails; +import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; +import jersey.repackaged.jsr166e.CompletableFuture; +import okhttp3.Call; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class IamTokenManager { + private String apiKey; + private String url; + private IamToken tokenData; + + public IamTokenManager(String accessToken, String apiKey, String refreshToken, String url) { + if (apiKey != null) { + this.apiKey = apiKey; + } + this.url = (url != null) ? url : "https://iam.ng.bluemix.net/identity/token"; + + tokenData = new IamToken.Builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + public String getToken() { + String token; + + if (tokenData.getAccessToken() != null) { + token = tokenData.getAccessToken(); + } else if (tokenData.getAccessToken() == null) { + tokenData = requestToken(null); + token = tokenData.getAccessToken(); + } else if (isTokenExpired()) { + tokenData = refreshToken(null); + token = tokenData.getAccessToken(); + } else { + token = tokenData.getAccessToken(); + } + + return token; + } + + public IamToken requestToken(String authorizationHeader) { + RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); + + builder.header("Content-Type", HttpMediaType.APPLICATION_FORM_URLENCODED); + builder.header("Authorization", (authorizationHeader != null) ? authorizationHeader : "Basic Yng6Yng="); + + //Map formObject = new HashMap<>(); + //final JsonObject jsonContent = new JsonObject(); + //jsonContent.addProperty("grant_type", "urn:ibm:params:oauth:grant-type:apikey"); + List formObject = new ArrayList<>(); + formObject.add(new NameValue("grant_type", "urn:ibm:params:oauth:grant-type:apikey")); + //formObject.put("grant_type", "urn:ibm:params:oauth:grant-type:apikey"); + //jsonContent.addProperty("apikey", apiKey); + //formObject.put("apikey", apiKey); + formObject.add(new NameValue("apikey", apiKey)); + //jsonContent.addProperty("response_type", "cloud_iam"); + //formObject.put("response_type", "cloud_iam"); + formObject.add(new NameValue("response_type", "cloud_iam")); + //builder.bodyJson(jsonContent); + builder.form(formObject); + + Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(builder.build()); + IamToken tokenTest = new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + return tokenTest; + //return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + } + + public IamToken refreshToken(String authorizationHeader) { + RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); + + builder.header("Content-Type", HttpMediaType.APPLICATION_FORM_URLENCODED); + builder.header("Authorization", (authorizationHeader != null) ? authorizationHeader : "Basic Yng6Yng="); + + final JsonObject jsonContent = new JsonObject(); + jsonContent.addProperty("grant_type", "refresh_token"); + jsonContent.addProperty("refresh_token", tokenData.getRefreshToken()); + //builder.bodyJson(jsonContent); + builder.form(jsonContent); + + Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(builder.build()); + return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + } + + private boolean isTokenExpired() { + Double fractionOfTimeToLive = 0.8; + Long timeToLive = tokenData.getExpiresIn(); + Long expirationTime = tokenData.getExpiration(); + + return (expirationTime - (timeToLive * (1.0 - fractionOfTimeToLive))) + < Math.floor(System.currentTimeMillis() / 1000); + } + + private class IamServiceCall implements ServiceCall { + + private Call call; + private ResponseConverter converter; + + IamServiceCall(Call call, ResponseConverter converter) { + this.call = call; + this.converter = converter; + } + + @Override + public ServiceCall addHeader(String name, String value) { + return null; + } + + @Override + public IamToken execute() throws RuntimeException { + try { + okhttp3.Response response = call.execute(); + return converter.convert(response); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Response executeWithDetails() throws RuntimeException { + return null; + } + + @Override + public void enqueue(ServiceCallback callback) { + + } + + @Override + public void enqueueWithDetails(ServiceCallbackWithDetails callback) { + + } + + @Override + public CompletableFuture rx() { + return null; + } + + @Override + public CompletableFuture> rxWithDetails() { + return null; + } + } + +} From da47bdc0e1743532df983e9b414445d4281ac7b3 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Tue, 17 Apr 2018 13:01:26 -0400 Subject: [PATCH 02/26] feat(assistant): Add testing lines for IAM support --- .../watson/developer_cloud/assistant/v1/Assistant.java | 6 ++++++ .../assistant/v1/AssistantServiceTest.java | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java index d2eb4cf52aa..9dd50233771 100644 --- a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java +++ b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java @@ -81,6 +81,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.CredentialOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import com.ibm.watson.developer_cloud.util.Validator; @@ -129,6 +130,11 @@ public Assistant(String versionDate, String username, String password) { setUsernameAndPassword(username, password); } + public Assistant(String versionDate, CredentialOptions credentialOptions) { + this(versionDate); + setCredentials(credentialOptions); + } + /** * Get a response to a user's input. There is no rate limit for this operation. * diff --git a/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java b/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java index 31aec04ae96..c04f98d11d4 100644 --- a/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java +++ b/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java @@ -13,6 +13,7 @@ package com.ibm.watson.developer_cloud.assistant.v1; import com.ibm.watson.developer_cloud.WatsonServiceTest; +import com.ibm.watson.developer_cloud.service.security.CredentialOptions; import org.junit.Assume; import org.junit.Before; @@ -38,9 +39,12 @@ public void setUp() throws Exception { Assume.assumeFalse("config.properties doesn't have valid credentials.", (username == null) || username.equals(PLACEHOLDER)); - service = new Assistant("2018-02-16"); + CredentialOptions options = new CredentialOptions.Builder() + .iamApiKey("cY2HWLnw-BFIyaw65ZgTtqciDE9oijwt4FL8vyz0zWgP") + .build(); + service = new Assistant("2018-02-16", options); service.setEndPoint(getProperty("conversation.v1.url")); - service.setUsernameAndPassword(username, password); + //service.setUsernameAndPassword(username, password); service.setDefaultHeaders(getDefaultHeaders()); } From 3a841e445a576f741a406f4445b6addda542b241 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 09:44:18 -0400 Subject: [PATCH 03/26] refactor(core): Use proper form to make IAM API call and switch to hardcoded auth header --- .../service/security/IamToken.java | 5 ++ .../service/security/IamTokenManager.java | 55 +++++++------------ 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java index d185ccd194a..3dc9d426dbd 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java @@ -1,11 +1,16 @@ package com.ibm.watson.developer_cloud.service.security; +import com.google.gson.annotations.SerializedName; import com.ibm.watson.developer_cloud.service.model.ObjectModel; class IamToken implements ObjectModel { + @SerializedName("access_token") private String accessToken; + @SerializedName("refresh_token") private String refreshToken; + @SerializedName("token_type") private String tokenType; + @SerializedName("expires_in") private Long expiresIn; private Long expiration; diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 2388a01f858..961cc54ca71 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -1,9 +1,7 @@ package com.ibm.watson.developer_cloud.service.security; -import com.google.gson.JsonObject; import com.ibm.watson.developer_cloud.http.HttpClientSingleton; import com.ibm.watson.developer_cloud.http.HttpMediaType; -import com.ibm.watson.developer_cloud.http.NameValue; import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.Response; import com.ibm.watson.developer_cloud.http.ResponseConverter; @@ -13,12 +11,9 @@ import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import jersey.repackaged.jsr166e.CompletableFuture; import okhttp3.Call; +import okhttp3.FormBody; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; public class IamTokenManager { private String apiKey; @@ -43,10 +38,10 @@ public String getToken() { if (tokenData.getAccessToken() != null) { token = tokenData.getAccessToken(); } else if (tokenData.getAccessToken() == null) { - tokenData = requestToken(null); + tokenData = requestToken(); token = tokenData.getAccessToken(); } else if (isTokenExpired()) { - tokenData = refreshToken(null); + tokenData = refreshToken(); token = tokenData.getAccessToken(); } else { token = tokenData.getAccessToken(); @@ -55,44 +50,34 @@ public String getToken() { return token; } - public IamToken requestToken(String authorizationHeader) { + public IamToken requestToken() { RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); builder.header("Content-Type", HttpMediaType.APPLICATION_FORM_URLENCODED); - builder.header("Authorization", (authorizationHeader != null) ? authorizationHeader : "Basic Yng6Yng="); - - //Map formObject = new HashMap<>(); - //final JsonObject jsonContent = new JsonObject(); - //jsonContent.addProperty("grant_type", "urn:ibm:params:oauth:grant-type:apikey"); - List formObject = new ArrayList<>(); - formObject.add(new NameValue("grant_type", "urn:ibm:params:oauth:grant-type:apikey")); - //formObject.put("grant_type", "urn:ibm:params:oauth:grant-type:apikey"); - //jsonContent.addProperty("apikey", apiKey); - //formObject.put("apikey", apiKey); - formObject.add(new NameValue("apikey", apiKey)); - //jsonContent.addProperty("response_type", "cloud_iam"); - //formObject.put("response_type", "cloud_iam"); - formObject.add(new NameValue("response_type", "cloud_iam")); - //builder.bodyJson(jsonContent); - builder.form(formObject); + builder.header("Authorization", "Basic Yng6Yng="); + + FormBody formBody = new FormBody.Builder() + .add("grant_type", "urn:ibm:params:oauth:grant-type:apikey") + .add("apikey", apiKey) + .add("response_type", "cloud_iam") + .build(); + builder.body(formBody); Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(builder.build()); - IamToken tokenTest = new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); - return tokenTest; - //return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); } - public IamToken refreshToken(String authorizationHeader) { + public IamToken refreshToken() { RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); builder.header("Content-Type", HttpMediaType.APPLICATION_FORM_URLENCODED); - builder.header("Authorization", (authorizationHeader != null) ? authorizationHeader : "Basic Yng6Yng="); + builder.header("Authorization", "Basic Yng6Yng="); - final JsonObject jsonContent = new JsonObject(); - jsonContent.addProperty("grant_type", "refresh_token"); - jsonContent.addProperty("refresh_token", tokenData.getRefreshToken()); - //builder.bodyJson(jsonContent); - builder.form(jsonContent); + FormBody formBody = new FormBody.Builder() + .add("grant_type", "refresh_token") + .add("refresh_token", tokenData.getRefreshToken()) + .build(); + builder.body(formBody); Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(builder.build()); return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); From a671643adf36c742b4d3eaa1041147b7f7e5f0e8 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 11:13:25 -0400 Subject: [PATCH 04/26] style(core): Add comments, style changes for clarity --- .../service/security/CredentialOptions.java | 12 ++++ .../service/security/IamToken.java | 15 ++++ .../service/security/IamTokenManager.java | 70 +++++++++++++++++-- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java index 3cffdf2ca20..6fc28319939 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java @@ -1,3 +1,15 @@ +/* + * Copyright 2018 IBM Corp. All Rights Reserved. + * + * 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 com.ibm.watson.developer_cloud.service.security; public class CredentialOptions { diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java index 3dc9d426dbd..dab176d07a7 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java @@ -1,8 +1,23 @@ +/* + * Copyright 2018 IBM Corp. All Rights Reserved. + * + * 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 com.ibm.watson.developer_cloud.service.security; import com.google.gson.annotations.SerializedName; import com.ibm.watson.developer_cloud.service.model.ObjectModel; +/** + * Represents response from IAM API. + */ class IamToken implements ObjectModel { @SerializedName("access_token") private String accessToken; diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 961cc54ca71..73c3e3ef17e 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -1,6 +1,19 @@ +/* + * Copyright 2018 IBM Corp. All Rights Reserved. + * + * 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 com.ibm.watson.developer_cloud.service.security; import com.ibm.watson.developer_cloud.http.HttpClientSingleton; +import com.ibm.watson.developer_cloud.http.HttpHeaders; import com.ibm.watson.developer_cloud.http.HttpMediaType; import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.Response; @@ -12,14 +25,18 @@ import jersey.repackaged.jsr166e.CompletableFuture; import okhttp3.Call; import okhttp3.FormBody; - import java.io.IOException; +/** + * Retrieves, stores, and refreshes IAM tokens. + */ public class IamTokenManager { private String apiKey; private String url; private IamToken tokenData; + private static final String DEFAULT_AUTHORIZATION = "Basic Yng6Yng="; + public IamTokenManager(String accessToken, String apiKey, String refreshToken, String url) { if (apiKey != null) { this.apiKey = apiKey; @@ -32,29 +49,47 @@ public IamTokenManager(String accessToken, String apiKey, String refreshToken, S .build(); } + /** + * This function returns an access token. The source of the token is determined by the following logic: + * 1. If user provides their own managed access token, assume it is valid and send it + * 2. If this class is managing tokens and does not yet have one, make a request for one + * 3. If this class is managing tokens and the token has expired, refresh it + * 4. If this class is managing tokens and has a valid token stored, send it + * + * @return the valid access token + */ public String getToken() { String token; if (tokenData.getAccessToken() != null) { + // use user-managed access token token = tokenData.getAccessToken(); } else if (tokenData.getAccessToken() == null) { + // request first-time token tokenData = requestToken(); token = tokenData.getAccessToken(); } else if (isTokenExpired()) { + // refresh current token tokenData = refreshToken(); token = tokenData.getAccessToken(); } else { + // use valid managed token token = tokenData.getAccessToken(); } return token; } + /** + * Request an IAM token using an API key. + * + * @return the new access token + */ public IamToken requestToken() { RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); - builder.header("Content-Type", HttpMediaType.APPLICATION_FORM_URLENCODED); - builder.header("Authorization", "Basic Yng6Yng="); + builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED); + builder.header(HttpHeaders.AUTHORIZATION, DEFAULT_AUTHORIZATION); FormBody formBody = new FormBody.Builder() .add("grant_type", "urn:ibm:params:oauth:grant-type:apikey") @@ -67,11 +102,16 @@ public IamToken requestToken() { return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); } + /** + * Refresh an IAM token using a refresh token. + * + * @return the new access token + */ public IamToken refreshToken() { RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); - builder.header("Content-Type", HttpMediaType.APPLICATION_FORM_URLENCODED); - builder.header("Authorization", "Basic Yng6Yng="); + builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED); + builder.header(HttpHeaders.AUTHORIZATION, DEFAULT_AUTHORIZATION); FormBody formBody = new FormBody.Builder() .add("grant_type", "refresh_token") @@ -83,13 +123,29 @@ public IamToken refreshToken() { return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); } + + /** + * Check if currently stored token is expired. + * + * Using a buffer to prevent the edge case of the + * token expiring before the request could be made. + * + * The buffer will be a fraction of the total TTL. Using 80%. + * + * @return whether the current managed token is expired or not + */ private boolean isTokenExpired() { + if (tokenData.getExpiresIn() == null || tokenData.getExpiration() == null) { + return true; + } + Double fractionOfTimeToLive = 0.8; Long timeToLive = tokenData.getExpiresIn(); Long expirationTime = tokenData.getExpiration(); + Double refreshTime = expirationTime - (timeToLive * (1.0 - fractionOfTimeToLive)); + Double currentTime = Math.floor(System.currentTimeMillis() / 1000); - return (expirationTime - (timeToLive * (1.0 - fractionOfTimeToLive))) - < Math.floor(System.currentTimeMillis() / 1000); + return refreshTime < currentTime; } private class IamServiceCall implements ServiceCall { From 5f8cf38ffa5fdd6115bc7ea19dbd9d07b10abc7f Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 11:17:04 -0400 Subject: [PATCH 05/26] refactor(core): Return just the access token string from requestToken() and refreshToken() --- .../service/security/IamTokenManager.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 73c3e3ef17e..d07dd9788ab 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -66,12 +66,10 @@ public String getToken() { token = tokenData.getAccessToken(); } else if (tokenData.getAccessToken() == null) { // request first-time token - tokenData = requestToken(); - token = tokenData.getAccessToken(); + token = requestToken(); } else if (isTokenExpired()) { // refresh current token - tokenData = refreshToken(); - token = tokenData.getAccessToken(); + token = refreshToken(); } else { // use valid managed token token = tokenData.getAccessToken(); @@ -81,11 +79,11 @@ public String getToken() { } /** - * Request an IAM token using an API key. + * Request an IAM token using an API key. Also updates internal managed IAM token information. * * @return the new access token */ - public IamToken requestToken() { + public String requestToken() { RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED); @@ -99,15 +97,16 @@ public IamToken requestToken() { builder.body(formBody); Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(builder.build()); - return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + tokenData = new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + return tokenData.getAccessToken(); } /** - * Refresh an IAM token using a refresh token. + * Refresh an IAM token using a refresh token. Also updates internal managed IAM token information. * * @return the new access token */ - public IamToken refreshToken() { + public String refreshToken() { RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED); @@ -120,7 +119,8 @@ public IamToken refreshToken() { builder.body(formBody); Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(builder.build()); - return new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + tokenData = new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + return tokenData.getAccessToken(); } From 8aaae7c8a2ba68401ef77c222fb6b9eb17a0b502 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 11:19:11 -0400 Subject: [PATCH 06/26] refactor(core): Use constructor instead of builder for IamToken --- .../service/security/IamToken.java | 35 +++++-------------- .../service/security/IamTokenManager.java | 5 +-- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java index dab176d07a7..78061fc0aae 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java @@ -29,47 +29,28 @@ class IamToken implements ObjectModel { private Long expiresIn; private Long expiration; - public static class Builder { - private String accessToken; - private String refreshToken; - - public IamToken build() { - return new IamToken(this); - } - - public Builder accessToken(String accessToken) { - this.accessToken = accessToken; - return this; - } - - public Builder refreshToken(String refreshToken) { - this.refreshToken = refreshToken; - return this; - } - } - - private IamToken(Builder builder) { - this.accessToken = builder.accessToken; - this.refreshToken = builder.refreshToken; + IamToken(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; } - public String getAccessToken() { + String getAccessToken() { return accessToken; } - public String getRefreshToken() { + String getRefreshToken() { return refreshToken; } - public String getTokenType() { + String getTokenType() { return tokenType; } - public Long getExpiresIn() { + Long getExpiresIn() { return expiresIn; } - public Long getExpiration() { + Long getExpiration() { return expiration; } } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index d07dd9788ab..a1a04a7ab84 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -43,10 +43,7 @@ public IamTokenManager(String accessToken, String apiKey, String refreshToken, S } this.url = (url != null) ? url : "https://iam.ng.bluemix.net/identity/token"; - tokenData = new IamToken.Builder() - .accessToken(accessToken) - .refreshToken(refreshToken) - .build(); + tokenData = new IamToken(accessToken, refreshToken); } /** From d39dbdfd79bd2618a3010260093926b41770ff8c Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 11:47:01 -0400 Subject: [PATCH 07/26] refactor(core): Change options model for IAM and keep user-managed token in IamTokenManager --- .../assistant/v1/Assistant.java | 6 +- .../assistant/v1/AssistantServiceTest.java | 4 +- .../service/WatsonService.java | 23 +------ ...CredentialOptions.java => IamOptions.java} | 61 +++++-------------- .../service/security/IamToken.java | 3 +- .../service/security/IamTokenManager.java | 17 +++--- 6 files changed, 31 insertions(+), 83 deletions(-) rename core/src/main/java/com/ibm/watson/developer_cloud/service/security/{CredentialOptions.java => IamOptions.java} (57%) diff --git a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java index 9dd50233771..de7c2899d06 100644 --- a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java +++ b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java @@ -81,7 +81,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; -import com.ibm.watson.developer_cloud.service.security.CredentialOptions; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import com.ibm.watson.developer_cloud.util.Validator; @@ -130,9 +130,9 @@ public Assistant(String versionDate, String username, String password) { setUsernameAndPassword(username, password); } - public Assistant(String versionDate, CredentialOptions credentialOptions) { + public Assistant(String versionDate, IamOptions iamOptions) { this(versionDate); - setCredentials(credentialOptions); + setIamCredentials(iamOptions); } /** diff --git a/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java b/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java index c04f98d11d4..d1e6bcaa990 100644 --- a/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java +++ b/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java @@ -13,7 +13,7 @@ package com.ibm.watson.developer_cloud.assistant.v1; import com.ibm.watson.developer_cloud.WatsonServiceTest; -import com.ibm.watson.developer_cloud.service.security.CredentialOptions; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import org.junit.Assume; import org.junit.Before; @@ -39,7 +39,7 @@ public void setUp() throws Exception { Assume.assumeFalse("config.properties doesn't have valid credentials.", (username == null) || username.equals(PLACEHOLDER)); - CredentialOptions options = new CredentialOptions.Builder() + IamOptions options = new IamOptions.Builder() .iamApiKey("cY2HWLnw-BFIyaw65ZgTtqciDE9oijwt4FL8vyz0zWgP") .build(); service = new Assistant("2018-02-16", options); diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index bcd2ef98fff..1cbaaef7a01 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -33,7 +33,7 @@ import com.ibm.watson.developer_cloud.service.exception.TooManyRequestsException; import com.ibm.watson.developer_cloud.service.exception.UnauthorizedException; import com.ibm.watson.developer_cloud.service.exception.UnsupportedException; -import com.ibm.watson.developer_cloud.service.security.CredentialOptions; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.service.security.IamTokenManager; import com.ibm.watson.developer_cloud.util.CredentialUtils; import com.ibm.watson.developer_cloud.util.RequestUtils; @@ -332,25 +332,8 @@ public void setDefaultHeaders(final Map headers) { } } - protected void setCredentials(CredentialOptions credentialOptions) { - - // setting username and password - if (credentialOptions.getUsername() != null && credentialOptions.getPassword() != null) { - setUsernameAndPassword(credentialOptions.getUsername(), credentialOptions.getPassword()); - - // setting API key - } else if (credentialOptions.getApiKey() != null) { - setApiKey(credentialOptions.getApiKey()); - - // setting IAM token - } else if (credentialOptions.getAccessToken() != null || credentialOptions.getIamApiKey() != null) { - this.tokenManager = new IamTokenManager( - credentialOptions.getAccessToken(), - credentialOptions.getIamApiKey(), - credentialOptions.getRefreshToken(), - credentialOptions.getIamUrl() - ); - } + protected void setIamCredentials(IamOptions iamOptions) { + this.tokenManager = new IamTokenManager(iamOptions); } /* diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java similarity index 57% rename from core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java rename to core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java index 6fc28319939..e62b3848870 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/CredentialOptions.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java @@ -12,31 +12,19 @@ */ package com.ibm.watson.developer_cloud.service.security; -public class CredentialOptions { - private String username; - private String password; +/** + * Options for authenticating using IAM. + */ +public class IamOptions { private String apiKey; - private String iamApiKey; private String accessToken; private String refreshToken; - private String iamUrl; - - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } + private String url; public String getApiKey() { return apiKey; } - public String getIamApiKey() { - return iamApiKey; - } - public String getAccessToken() { return accessToken; } @@ -45,31 +33,18 @@ public String getRefreshToken() { return refreshToken; } - public String getIamUrl() { - return iamUrl; + public String getUrl() { + return url; } public static class Builder { - private String username; - private String password; private String apiKey; - private String iamApiKey; private String accessToken; private String refreshToken; - private String iamUrl; - - public CredentialOptions build() { - return new CredentialOptions(this); - } - - public Builder username(String username) { - this.username = username; - return this; - } + private String url; - public Builder password(String password) { - this.password = password; - return this; + public IamOptions build() { + return new IamOptions(this); } public Builder apiKey(String apiKey) { @@ -77,11 +52,6 @@ public Builder apiKey(String apiKey) { return this; } - public Builder iamApiKey(String iamApiKey) { - this.iamApiKey = iamApiKey; - return this; - } - public Builder accessToken(String accessToken) { this.accessToken = accessToken; return this; @@ -92,19 +62,16 @@ public Builder refreshToken(String refreshToken) { return this; } - public Builder iamUrl(String iamUrl) { - this.iamUrl = iamUrl; + public Builder url(String url) { + this.url = url; return this; } } - private CredentialOptions(Builder builder) { - this.username = builder.username; - this.password = builder.password; + private IamOptions(Builder builder) { this.apiKey = builder.apiKey; - this.iamApiKey = builder.iamApiKey; this.accessToken = builder.accessToken; this.refreshToken = builder.refreshToken; - this.iamUrl = builder.iamUrl; + this.url = builder.url; } } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java index 78061fc0aae..accc04bbe35 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java @@ -29,8 +29,7 @@ class IamToken implements ObjectModel { private Long expiresIn; private Long expiration; - IamToken(String accessToken, String refreshToken) { - this.accessToken = accessToken; + IamToken(String refreshToken) { this.refreshToken = refreshToken; } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index a1a04a7ab84..6f09aca7dd7 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -31,19 +31,18 @@ * Retrieves, stores, and refreshes IAM tokens. */ public class IamTokenManager { + private String userManagedAccessToken; private String apiKey; private String url; private IamToken tokenData; private static final String DEFAULT_AUTHORIZATION = "Basic Yng6Yng="; - public IamTokenManager(String accessToken, String apiKey, String refreshToken, String url) { - if (apiKey != null) { - this.apiKey = apiKey; - } - this.url = (url != null) ? url : "https://iam.ng.bluemix.net/identity/token"; - - tokenData = new IamToken(accessToken, refreshToken); + public IamTokenManager(IamOptions options) { + this.apiKey = options.getApiKey(); + this.url = (options.getUrl() != null) ? options.getUrl() : "https://iam.ng.bluemix.net/identity/token"; + this.userManagedAccessToken = options.getAccessToken(); + tokenData = new IamToken(options.getRefreshToken()); } /** @@ -58,9 +57,9 @@ public IamTokenManager(String accessToken, String apiKey, String refreshToken, S public String getToken() { String token; - if (tokenData.getAccessToken() != null) { + if (userManagedAccessToken != null) { // use user-managed access token - token = tokenData.getAccessToken(); + token = userManagedAccessToken; } else if (tokenData.getAccessToken() == null) { // request first-time token token = requestToken(); From 1bc675e18281e83bd99da620fe97ee892d69ac78 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 11:50:54 -0400 Subject: [PATCH 08/26] refactor(core): Don't accept refresh token when instantiating service with IAM --- .../developer_cloud/service/security/IamOptions.java | 12 ------------ .../developer_cloud/service/security/IamToken.java | 4 ---- .../service/security/IamTokenManager.java | 1 - 3 files changed, 17 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java index e62b3848870..3aa34f43027 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java @@ -18,7 +18,6 @@ public class IamOptions { private String apiKey; private String accessToken; - private String refreshToken; private String url; public String getApiKey() { @@ -29,10 +28,6 @@ public String getAccessToken() { return accessToken; } - public String getRefreshToken() { - return refreshToken; - } - public String getUrl() { return url; } @@ -40,7 +35,6 @@ public String getUrl() { public static class Builder { private String apiKey; private String accessToken; - private String refreshToken; private String url; public IamOptions build() { @@ -57,11 +51,6 @@ public Builder accessToken(String accessToken) { return this; } - public Builder refreshToken(String refreshToken) { - this.refreshToken = refreshToken; - return this; - } - public Builder url(String url) { this.url = url; return this; @@ -71,7 +60,6 @@ public Builder url(String url) { private IamOptions(Builder builder) { this.apiKey = builder.apiKey; this.accessToken = builder.accessToken; - this.refreshToken = builder.refreshToken; this.url = builder.url; } } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java index accc04bbe35..e697394adb2 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java @@ -29,10 +29,6 @@ class IamToken implements ObjectModel { private Long expiresIn; private Long expiration; - IamToken(String refreshToken) { - this.refreshToken = refreshToken; - } - String getAccessToken() { return accessToken; } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 6f09aca7dd7..ca5216fdd10 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -42,7 +42,6 @@ public IamTokenManager(IamOptions options) { this.apiKey = options.getApiKey(); this.url = (options.getUrl() != null) ? options.getUrl() : "https://iam.ng.bluemix.net/identity/token"; this.userManagedAccessToken = options.getAccessToken(); - tokenData = new IamToken(options.getRefreshToken()); } /** From 9b49c96d06063de31ee2ef8893af8cf26fb7445b Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 13:18:31 -0400 Subject: [PATCH 09/26] refactor(core): Replace IAM ServiceCall implementation with a single method --- .../assistant/v1/AssistantServiceTest.java | 2 +- .../service/WatsonService.java | 5 ++ .../service/security/IamTokenManager.java | 78 +++++-------------- 3 files changed, 25 insertions(+), 60 deletions(-) diff --git a/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java b/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java index d1e6bcaa990..bab801b98a2 100644 --- a/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java +++ b/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java @@ -40,7 +40,7 @@ public void setUp() throws Exception { (username == null) || username.equals(PLACEHOLDER)); IamOptions options = new IamOptions.Builder() - .iamApiKey("cY2HWLnw-BFIyaw65ZgTtqciDE9oijwt4FL8vyz0zWgP") + .apiKey("cY2HWLnw-BFIyaw65ZgTtqciDE9oijwt4FL8vyz0zWgP") .build(); service = new Assistant("2018-02-16", options); service.setEndPoint(getProperty("conversation.v1.url")); diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index 1cbaaef7a01..cc70c4d0c28 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -332,6 +332,11 @@ public void setDefaultHeaders(final Map headers) { } } + /** + * Sets IAM information. + * + * @param iamOptions object containing values to be used for authenticating with IAM + */ protected void setIamCredentials(IamOptions iamOptions) { this.tokenManager = new IamTokenManager(iamOptions); } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index ca5216fdd10..06eff0704af 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -16,15 +16,12 @@ import com.ibm.watson.developer_cloud.http.HttpHeaders; import com.ibm.watson.developer_cloud.http.HttpMediaType; import com.ibm.watson.developer_cloud.http.RequestBuilder; -import com.ibm.watson.developer_cloud.http.Response; import com.ibm.watson.developer_cloud.http.ResponseConverter; -import com.ibm.watson.developer_cloud.http.ServiceCall; -import com.ibm.watson.developer_cloud.http.ServiceCallback; -import com.ibm.watson.developer_cloud.http.ServiceCallbackWithDetails; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; -import jersey.repackaged.jsr166e.CompletableFuture; import okhttp3.Call; import okhttp3.FormBody; +import okhttp3.Request; + import java.io.IOException; /** @@ -91,8 +88,7 @@ public String requestToken() { .build(); builder.body(formBody); - Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(builder.build()); - tokenData = new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + tokenData = callIamApi(builder.build()); return tokenData.getAccessToken(); } @@ -113,12 +109,10 @@ public String refreshToken() { .build(); builder.body(formBody); - Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(builder.build()); - tokenData = new IamServiceCall<>(call, ResponseConverterUtils.getObject(IamToken.class)).execute(); + tokenData = callIamApi(builder.build()); return tokenData.getAccessToken(); } - /** * Check if currently stored token is expired. * @@ -143,55 +137,21 @@ private boolean isTokenExpired() { return refreshTime < currentTime; } - private class IamServiceCall implements ServiceCall { - - private Call call; - private ResponseConverter converter; - - IamServiceCall(Call call, ResponseConverter converter) { - this.call = call; - this.converter = converter; - } - - @Override - public ServiceCall addHeader(String name, String value) { - return null; - } - - @Override - public IamToken execute() throws RuntimeException { - try { - okhttp3.Response response = call.execute(); - return converter.convert(response); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public Response executeWithDetails() throws RuntimeException { - return null; - } - - @Override - public void enqueue(ServiceCallback callback) { - - } - - @Override - public void enqueueWithDetails(ServiceCallbackWithDetails callback) { - - } - - @Override - public CompletableFuture rx() { - return null; - } - - @Override - public CompletableFuture> rxWithDetails() { - return null; + /** + * Executes call to IAM API and returns IamToken object representing the response. + * + * @param request the request for the IAM API + * @return object containing requested IAM token information + */ + private IamToken callIamApi(Request request) { + Call call = HttpClientSingleton.getInstance().createHttpClient().newCall(request); + ResponseConverter converter = ResponseConverterUtils.getObject(IamToken.class); + + try { + okhttp3.Response response = call.execute(); + return converter.convert(response); + } catch (IOException e) { + throw new RuntimeException(e); } } - } From 9b621ad15c566ff94c049db40c4c8d3e1ebe3fc2 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 13:28:35 -0400 Subject: [PATCH 10/26] feat(core): Add method to set user-managed access token outside of constructor --- .../service/security/IamOptions.java | 4 ++++ .../service/security/IamTokenManager.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java index 3aa34f43027..d4a544c721b 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java @@ -14,6 +14,10 @@ /** * Options for authenticating using IAM. + * + * Be aware that if you decide to pass in an access token, you accept responsibility for managing the access token + * yourself. You must set a new access token before this one expires. Failing to do so will result in authentication + * errors after this token expires. */ public class IamOptions { private String apiKey; diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 06eff0704af..74e9366f3dd 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -113,6 +113,20 @@ public String refreshToken() { return tokenData.getAccessToken(); } + /** + * Set a self-managed IAM access token. + * The access token should be valid and not yet expired. + * + * By using this method, you accept responsibility for managing the access token yourself. You must set a new + * access token before this one expires. Failing to do so will result in authentication errors after this token + * expires. + * + * @param userManagedAccessToken a valid, non-expired IAM access token + */ + public void setAccessToken(String userManagedAccessToken) { + this.userManagedAccessToken = userManagedAccessToken; + } + /** * Check if currently stored token is expired. * From 3dffb61159a143aea17b375780902b98ec57f548 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 13:57:19 -0400 Subject: [PATCH 11/26] refactor(core): Tweak manner of setting IAM credentials from user POV and add clear warnings --- .../developer_cloud/assistant/v1/Assistant.java | 9 +++++++++ .../developer_cloud/service/WatsonService.java | 6 +++++- .../service/security/IamOptions.java | 4 ---- .../service/security/IamTokenManager.java | 14 -------------- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java index de7c2899d06..35b18623600 100644 --- a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java +++ b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java @@ -130,6 +130,15 @@ public Assistant(String versionDate, String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `Assistant` with IAM. Note that if the access token is specified in the iamOptions, you accept + * responsibility for managing the access token yourself. You must set a new access token before this one expires. + * Failing to do so will result in authentication errors after this token expires. + * + * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API + * calls from failing when the service introduces breaking changes. + * @param iamOptions the options for authenticating through IAM + */ public Assistant(String versionDate, IamOptions iamOptions) { this(versionDate); setIamCredentials(iamOptions); diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index cc70c4d0c28..71b28467cb5 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -335,9 +335,13 @@ public void setDefaultHeaders(final Map headers) { /** * Sets IAM information. * + * Be aware that if you pass in an access token using this method, you accept responsibility for managing the access + * token yourself. You must set a new access token before this one expires. Failing to do so will result in + * authentication errors after this token expires. + * * @param iamOptions object containing values to be used for authenticating with IAM */ - protected void setIamCredentials(IamOptions iamOptions) { + public void setIamCredentials(IamOptions iamOptions) { this.tokenManager = new IamTokenManager(iamOptions); } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java index d4a544c721b..3aa34f43027 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java @@ -14,10 +14,6 @@ /** * Options for authenticating using IAM. - * - * Be aware that if you decide to pass in an access token, you accept responsibility for managing the access token - * yourself. You must set a new access token before this one expires. Failing to do so will result in authentication - * errors after this token expires. */ public class IamOptions { private String apiKey; diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 74e9366f3dd..06eff0704af 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -113,20 +113,6 @@ public String refreshToken() { return tokenData.getAccessToken(); } - /** - * Set a self-managed IAM access token. - * The access token should be valid and not yet expired. - * - * By using this method, you accept responsibility for managing the access token yourself. You must set a new - * access token before this one expires. Failing to do so will result in authentication errors after this token - * expires. - * - * @param userManagedAccessToken a valid, non-expired IAM access token - */ - public void setAccessToken(String userManagedAccessToken) { - this.userManagedAccessToken = userManagedAccessToken; - } - /** * Check if currently stored token is expired. * From 0c86ade2462a90d7ec2d21920d46f879fa80f32f Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 15:17:43 -0400 Subject: [PATCH 12/26] feat(core): Get IAM API key from VCAP_SERVICES if available --- .../service/WatsonService.java | 4 ++++ .../developer_cloud/util/CredentialUtils.java | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index 71b28467cb5..9a916717e50 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -100,6 +100,10 @@ public abstract class WatsonService { */ public WatsonService(final String name) { this.name = name; + String iamApiKey = CredentialUtils.getIamAPIKey(name); + if (iamApiKey != null) { + tokenManager = new IamTokenManager(new IamOptions.Builder().apiKey(iamApiKey).build()); + } apiKey = CredentialUtils.getAPIKey(name); String url = CredentialUtils.getAPIUrl(name); if ((url != null) && !url.isEmpty()) { diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java b/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java index 9e1bc90aa49..4f266e831bd 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java @@ -191,6 +191,27 @@ private static JsonObject getVCAPServices() { return vcapServices; } + /** + * Returns the IAM API key from the VCAP_SERVICES, or null if it doesn't exist. + * + * @param serviceName the service name + * @return the IAM API key or null if the service cannot be found + */ + public static String getIamAPIKey(String serviceName) { + final JsonObject services = getVCAPServices(); + + if (serviceName == null || services == null) { + return null; + } + + final JsonObject credentials = getCredentialsObject(services, serviceName, null); + if (credentials != null && credentials.get(APIKEY) != null && credentials.get("iam_apikey_name") != null) { + return credentials.get(APIKEY).getAsString(); + } + + return null; + } + /** * Returns the apiKey from the VCAP_SERVICES or null if doesn't exists. * From 9e60eeb3278c50911ec74bbbf485bbaa846ecc3d Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 15:29:56 -0400 Subject: [PATCH 13/26] test(core): Add test for getting IAM API key from VCAP_SERVICES --- .../developer_cloud/service/WatsonService.java | 2 +- .../developer_cloud/util/CredentialUtils.java | 2 +- .../developer_cloud/util/CredentialUtilsTest.java | 13 +++++++++++++ core/src/test/resources/vcap_services.json | 15 +++++++++------ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index 9a916717e50..265ccfde690 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -100,7 +100,7 @@ public abstract class WatsonService { */ public WatsonService(final String name) { this.name = name; - String iamApiKey = CredentialUtils.getIamAPIKey(name); + String iamApiKey = CredentialUtils.getIAMKey(name); if (iamApiKey != null) { tokenManager = new IamTokenManager(new IamOptions.Builder().apiKey(iamApiKey).build()); } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java b/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java index 4f266e831bd..13e469c5275 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java @@ -197,7 +197,7 @@ private static JsonObject getVCAPServices() { * @param serviceName the service name * @return the IAM API key or null if the service cannot be found */ - public static String getIamAPIKey(String serviceName) { + public static String getIAMKey(String serviceName) { final JsonObject services = getVCAPServices(); if (serviceName == null || services == null) { diff --git a/core/src/test/java/com/ibm/watson/developer_cloud/util/CredentialUtilsTest.java b/core/src/test/java/com/ibm/watson/developer_cloud/util/CredentialUtilsTest.java index 5a2d6ff8e29..1231ae21285 100644 --- a/core/src/test/java/com/ibm/watson/developer_cloud/util/CredentialUtilsTest.java +++ b/core/src/test/java/com/ibm/watson/developer_cloud/util/CredentialUtilsTest.java @@ -13,6 +13,7 @@ package com.ibm.watson.developer_cloud.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -54,6 +55,9 @@ public class CredentialUtilsTest extends WatsonServiceTest { private static final String PERSONALITY_INSIGHTS_URL = "https://gateway.watsonplatform.net/personality-insights/api"; + private static final String IAM_SERVICE_NAME = "language_translator"; + private static final String IAM_KEY_TEST_VALUE = "123456789"; + /** * Setup. */ @@ -125,6 +129,15 @@ public void testGetUserCredentialsWithPlan() { assertEquals(credentials.getPassword(), NOT_A_PASSWORD); } + /** + * Test getting IAM API key from VCAP_SERVICES. + */ + @Test + public void testGetIAMKey() { + String key = CredentialUtils.getIAMKey(IAM_SERVICE_NAME); + assertEquals(IAM_KEY_TEST_VALUE, key); + } + /** * Test getting the API URL using JDNI. We ignore this test in Travis because * it always fails there. diff --git a/core/src/test/resources/vcap_services.json b/core/src/test/resources/vcap_services.json index 2b3cdeea9c1..f4f698594f9 100644 --- a/core/src/test/resources/vcap_services.json +++ b/core/src/test/resources/vcap_services.json @@ -59,15 +59,18 @@ } } ], - "language_translation": [ + "language_translator": [ { - "name": "language_translation_docs", - "label": "language_translation", + "name": "language_translator_docs", + "label": "language_translator", "plan": "standard", "credentials": { - "url": "https://gateway.watsonplatform.net/language-translation/api", - "username": "not-a-username", - "password": "not-a-password" + "apikey": "123456789", + "iam_apikey_description": "Auto generated apikey...", + "iam_apikey_name": "auto-generated-apikey-111-222-333", + "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager", + "iam_serviceid_crn": "crn:v1:staging:public:iam-identity::a/::serviceid:ServiceID-1234", + "url": "https://gateway.watsonplatform.net/language-translator/api" } } ], From 06f61dd56d9129d77c8cac3106c23fca992df54d Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 16:44:47 -0400 Subject: [PATCH 14/26] test(core): Add remaining IAM unit tests --- .../service/security/IamToken.java | 12 +- .../service/security/IamTokenManager.java | 15 +++ .../service/IamManagerTest.java | 113 ++++++++++++++++++ .../util/CredentialUtilsTest.java | 18 ++- .../src/test/resources/expired_iam_token.json | 7 ++ core/src/test/resources/valid_iam_token.json | 7 ++ 6 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java create mode 100644 core/src/test/resources/expired_iam_token.json create mode 100644 core/src/test/resources/valid_iam_token.json diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java index e697394adb2..ed09149863a 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java @@ -18,7 +18,7 @@ /** * Represents response from IAM API. */ -class IamToken implements ObjectModel { +public class IamToken implements ObjectModel { @SerializedName("access_token") private String accessToken; @SerializedName("refresh_token") @@ -29,23 +29,23 @@ class IamToken implements ObjectModel { private Long expiresIn; private Long expiration; - String getAccessToken() { + public String getAccessToken() { return accessToken; } - String getRefreshToken() { + public String getRefreshToken() { return refreshToken; } - String getTokenType() { + public String getTokenType() { return tokenType; } - Long getExpiresIn() { + public Long getExpiresIn() { return expiresIn; } - Long getExpiration() { + public Long getExpiration() { return expiration; } } diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 06eff0704af..4489d5f9103 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -39,6 +39,7 @@ public IamTokenManager(IamOptions options) { this.apiKey = options.getApiKey(); this.url = (options.getUrl() != null) ? options.getUrl() : "https://iam.ng.bluemix.net/identity/token"; this.userManagedAccessToken = options.getAccessToken(); + tokenData = new IamToken(); } /** @@ -113,6 +114,20 @@ public String refreshToken() { return tokenData.getAccessToken(); } + /** + * Set a self-managed IAM access token. + * The access token should be valid and not yet expired. + * + * By using this method, you accept responsibility for managing the access token yourself. You must set a new + * access token before this one expires. Failing to do so will result in authentication errors after this token + * expires. + * + * @param userManagedAccessToken a valid, non-expired IAM access token + */ + public void setAccessToken(String userManagedAccessToken) { + this.userManagedAccessToken = userManagedAccessToken; + } + /** * Check if currently stored token is expired. * diff --git a/core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java b/core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java new file mode 100644 index 00000000000..6ced508c169 --- /dev/null +++ b/core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java @@ -0,0 +1,113 @@ +/** + * Copyright 2018 IBM Corp. All Rights Reserved. + * + * 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 com.ibm.watson.developer_cloud.service; + +import com.ibm.watson.developer_cloud.WatsonServiceUnitTest; +import com.ibm.watson.developer_cloud.service.security.IamOptions; +import com.ibm.watson.developer_cloud.service.security.IamToken; +import com.ibm.watson.developer_cloud.service.security.IamTokenManager; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class IamManagerTest extends WatsonServiceUnitTest { + + private IamToken expiredTokenData; + private IamToken validTokenData; + private String url; + + private static final String ACCESS_TOKEN = "abcd-1234"; + private static final String API_KEY = "123456789"; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + url = getMockWebServerUrl(); + expiredTokenData = loadFixture("src/test/resources/expired_iam_token.json", IamToken.class); + validTokenData = loadFixture("src/test/resources/valid_iam_token.json", IamToken.class); + } + + /** + * Tests that if a user passes in an access token during initial IAM setup, that access token is passed back + * during later retrieval. + */ + @Test + public void getUserManagedTokenFromConstructor() { + IamOptions options = new IamOptions.Builder() + .accessToken(ACCESS_TOKEN) + .url(url) + .build(); + IamTokenManager manager = new IamTokenManager(options); + + String token = manager.getToken(); + assertEquals(ACCESS_TOKEN, token); + } + + /** + * Tests that if a user sets their own access token at some point, that access token gets passed back during later + * retrieval. + */ + @Test + public void getUserManagedTokenFromMethodCall() { + IamOptions options = new IamOptions.Builder() + .apiKey(API_KEY) + .url(url) + .build(); + IamTokenManager manager = new IamTokenManager(options); + manager.setAccessToken(ACCESS_TOKEN); + + String token = manager.getToken(); + assertEquals(ACCESS_TOKEN, token); + } + + /** + * Tests that if only an API key is stored, the user can get back a valid access token. + */ + @Test + public void getTokenFromApiKey() throws InterruptedException { + server.enqueue(jsonResponse(validTokenData)); + + IamOptions options = new IamOptions.Builder() + .apiKey(API_KEY) + .url(url) + .build(); + IamTokenManager manager = new IamTokenManager(options); + + String token = manager.getToken(); + assertEquals(validTokenData.getAccessToken(), token); + } + + /** + * Tests that if the stored access token is expired, it can be refreshed properly. + */ + @Test + public void getTokenAfterRefresh() { + server.enqueue(jsonResponse(expiredTokenData)); + + IamOptions options = new IamOptions.Builder() + .apiKey(API_KEY) + .url(url) + .build(); + IamTokenManager manager = new IamTokenManager(options); + + // setting expired token + manager.getToken(); + + // getting valid token + server.enqueue(jsonResponse(validTokenData)); + String newToken = manager.getToken(); + + assertEquals(validTokenData.getAccessToken(), newToken); + } +} diff --git a/core/src/test/java/com/ibm/watson/developer_cloud/util/CredentialUtilsTest.java b/core/src/test/java/com/ibm/watson/developer_cloud/util/CredentialUtilsTest.java index 1231ae21285..ee1e0437a7d 100644 --- a/core/src/test/java/com/ibm/watson/developer_cloud/util/CredentialUtilsTest.java +++ b/core/src/test/java/com/ibm/watson/developer_cloud/util/CredentialUtilsTest.java @@ -12,21 +12,19 @@ */ package com.ibm.watson.developer_cloud.util; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.io.InputStream; -import java.util.Hashtable; - +import com.ibm.watson.developer_cloud.WatsonServiceTest; +import com.ibm.watson.developer_cloud.util.CredentialUtils.ServiceCredentials; import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import com.ibm.watson.developer_cloud.WatsonServiceTest; -import com.ibm.watson.developer_cloud.util.CredentialUtils.ServiceCredentials; +import java.io.InputStream; +import java.util.Hashtable; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * The Class CredentialUtilsTest. diff --git a/core/src/test/resources/expired_iam_token.json b/core/src/test/resources/expired_iam_token.json new file mode 100644 index 00000000000..bce563ba19c --- /dev/null +++ b/core/src/test/resources/expired_iam_token.json @@ -0,0 +1,7 @@ +{ + "access_token": "wxyz-9876", + "refresh_token": "00000000", + "token_type": "Bearer", + "expires_in": 3600, + "expiration": 1522788645 +} \ No newline at end of file diff --git a/core/src/test/resources/valid_iam_token.json b/core/src/test/resources/valid_iam_token.json new file mode 100644 index 00000000000..7fed350545c --- /dev/null +++ b/core/src/test/resources/valid_iam_token.json @@ -0,0 +1,7 @@ +{ + "access_token": "aaaa-1111", + "refresh_token": "99999999", + "token_type": "Bearer", + "expires_in": 3600, + "expiration": 999999999999999999 +} \ No newline at end of file From 170b94e92952a57a0c816ba22172f948114f6de7 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Wed, 18 Apr 2018 16:58:53 -0400 Subject: [PATCH 15/26] refactor(assistant): Remove changes for testing specifically in Assistant --- .../developer_cloud/assistant/v1/Assistant.java | 15 --------------- .../assistant/v1/AssistantServiceTest.java | 8 ++------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java index 35b18623600..d2eb4cf52aa 100644 --- a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java +++ b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java @@ -81,7 +81,6 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; -import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import com.ibm.watson.developer_cloud.util.Validator; @@ -130,20 +129,6 @@ public Assistant(String versionDate, String username, String password) { setUsernameAndPassword(username, password); } - /** - * Instantiates a new `Assistant` with IAM. Note that if the access token is specified in the iamOptions, you accept - * responsibility for managing the access token yourself. You must set a new access token before this one expires. - * Failing to do so will result in authentication errors after this token expires. - * - * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API - * calls from failing when the service introduces breaking changes. - * @param iamOptions the options for authenticating through IAM - */ - public Assistant(String versionDate, IamOptions iamOptions) { - this(versionDate); - setIamCredentials(iamOptions); - } - /** * Get a response to a user's input. There is no rate limit for this operation. * diff --git a/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java b/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java index bab801b98a2..31aec04ae96 100644 --- a/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java +++ b/assistant/src/test/java/com/ibm/watson/developer_cloud/assistant/v1/AssistantServiceTest.java @@ -13,7 +13,6 @@ package com.ibm.watson.developer_cloud.assistant.v1; import com.ibm.watson.developer_cloud.WatsonServiceTest; -import com.ibm.watson.developer_cloud.service.security.IamOptions; import org.junit.Assume; import org.junit.Before; @@ -39,12 +38,9 @@ public void setUp() throws Exception { Assume.assumeFalse("config.properties doesn't have valid credentials.", (username == null) || username.equals(PLACEHOLDER)); - IamOptions options = new IamOptions.Builder() - .apiKey("cY2HWLnw-BFIyaw65ZgTtqciDE9oijwt4FL8vyz0zWgP") - .build(); - service = new Assistant("2018-02-16", options); + service = new Assistant("2018-02-16"); service.setEndPoint(getProperty("conversation.v1.url")); - //service.setUsernameAndPassword(username, password); + service.setUsernameAndPassword(username, password); service.setDefaultHeaders(getDefaultHeaders()); } From 722ca5c7f70774beb93a447ce25ae2073f5ecaf8 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Thu, 19 Apr 2018 10:12:32 -0400 Subject: [PATCH 16/26] feat(service-constructors): Add constructors for IAM authentication --- .../developer_cloud/assistant/v1/Assistant.java | 15 +++++++++++++++ .../conversation/v1/Conversation.java | 15 +++++++++++++++ .../developer_cloud/discovery/v1/Discovery.java | 15 +++++++++++++++ .../v2/LanguageTranslator.java | 13 +++++++++++++ .../v1/NaturalLanguageClassifier.java | 13 +++++++++++++ .../v1/NaturalLanguageUnderstanding.java | 15 +++++++++++++++ .../v3/PersonalityInsights.java | 15 +++++++++++++++ .../speech_to_text/v1/SpeechToText.java | 13 +++++++++++++ .../text_to_speech/v1/TextToSpeech.java | 13 +++++++++++++ .../tone_analyzer/v3/ToneAnalyzer.java | 15 +++++++++++++++ .../visual_recognition/v3/VisualRecognition.java | 15 +++++++++++++++ 11 files changed, 157 insertions(+) diff --git a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java index d2eb4cf52aa..c0b76cd4750 100644 --- a/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java +++ b/assistant/src/main/java/com/ibm/watson/developer_cloud/assistant/v1/Assistant.java @@ -81,6 +81,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import com.ibm.watson.developer_cloud.util.Validator; @@ -129,6 +130,20 @@ public Assistant(String versionDate, String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `Assistant` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API + * calls from failing when the service introduces breaking changes. + * @param iamOptions the options for authenticating through IAM + */ + public Assistant(String versionDate, IamOptions iamOptions) { + this(versionDate); + setIamCredentials(iamOptions); + } + /** * Get a response to a user's input. There is no rate limit for this operation. * diff --git a/conversation/src/main/java/com/ibm/watson/developer_cloud/conversation/v1/Conversation.java b/conversation/src/main/java/com/ibm/watson/developer_cloud/conversation/v1/Conversation.java index 597827aae86..4639eab0c2f 100644 --- a/conversation/src/main/java/com/ibm/watson/developer_cloud/conversation/v1/Conversation.java +++ b/conversation/src/main/java/com/ibm/watson/developer_cloud/conversation/v1/Conversation.java @@ -81,6 +81,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import com.ibm.watson.developer_cloud.util.Validator; @@ -129,6 +130,20 @@ public Conversation(String versionDate, String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `Conversation` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API + * calls from failing when the service introduces breaking changes. + * @param iamOptions the options for authenticating through IAM + */ + public Conversation(String versionDate, IamOptions iamOptions) { + this(versionDate); + setIamCredentials(iamOptions); + } + /** * Create workspace. * diff --git a/discovery/src/main/java/com/ibm/watson/developer_cloud/discovery/v1/Discovery.java b/discovery/src/main/java/com/ibm/watson/developer_cloud/discovery/v1/Discovery.java index c54866f46ea..cdb1a0bdd02 100644 --- a/discovery/src/main/java/com/ibm/watson/developer_cloud/discovery/v1/Discovery.java +++ b/discovery/src/main/java/com/ibm/watson/developer_cloud/discovery/v1/Discovery.java @@ -76,6 +76,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.RequestUtils; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; @@ -129,6 +130,20 @@ public Discovery(String versionDate, String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `Discovery` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API + * calls from failing when the service introduces breaking changes. + * @param iamOptions the options for authenticating through IAM + */ + public Discovery(String versionDate, IamOptions iamOptions) { + this(versionDate); + setIamCredentials(iamOptions); + } + /** * Add an environment. * diff --git a/language-translator/src/main/java/com/ibm/watson/developer_cloud/language_translator/v2/LanguageTranslator.java b/language-translator/src/main/java/com/ibm/watson/developer_cloud/language_translator/v2/LanguageTranslator.java index 10d1d7c7615..a23adf8ac84 100644 --- a/language-translator/src/main/java/com/ibm/watson/developer_cloud/language_translator/v2/LanguageTranslator.java +++ b/language-translator/src/main/java/com/ibm/watson/developer_cloud/language_translator/v2/LanguageTranslator.java @@ -28,6 +28,7 @@ import com.ibm.watson.developer_cloud.language_translator.v2.model.TranslationModels; import com.ibm.watson.developer_cloud.language_translator.v2.model.TranslationResult; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.RequestUtils; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; @@ -70,6 +71,18 @@ public LanguageTranslator(String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `LanguageTranslator` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param iamOptions the options for authenticating through IAM + */ + public LanguageTranslator(IamOptions iamOptions) { + this(); + setIamCredentials(iamOptions); + } + /** * Translate. * diff --git a/natural-language-classifier/src/main/java/com/ibm/watson/developer_cloud/natural_language_classifier/v1/NaturalLanguageClassifier.java b/natural-language-classifier/src/main/java/com/ibm/watson/developer_cloud/natural_language_classifier/v1/NaturalLanguageClassifier.java index 3d191b5ff3b..3c6fbf24dec 100644 --- a/natural-language-classifier/src/main/java/com/ibm/watson/developer_cloud/natural_language_classifier/v1/NaturalLanguageClassifier.java +++ b/natural-language-classifier/src/main/java/com/ibm/watson/developer_cloud/natural_language_classifier/v1/NaturalLanguageClassifier.java @@ -26,6 +26,7 @@ import com.ibm.watson.developer_cloud.natural_language_classifier.v1.model.GetClassifierOptions; import com.ibm.watson.developer_cloud.natural_language_classifier.v1.model.ListClassifiersOptions; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.RequestUtils; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; @@ -75,6 +76,18 @@ public NaturalLanguageClassifier(String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `NaturalLanguageClassifier` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param iamOptions the options for authenticating through IAM + */ + public NaturalLanguageClassifier(IamOptions iamOptions) { + this(); + setIamCredentials(iamOptions); + } + /** * Classify a phrase. * diff --git a/natural-language-understanding/src/main/java/com/ibm/watson/developer_cloud/natural_language_understanding/v1/NaturalLanguageUnderstanding.java b/natural-language-understanding/src/main/java/com/ibm/watson/developer_cloud/natural_language_understanding/v1/NaturalLanguageUnderstanding.java index 820e2c60616..2b5a0b023f9 100644 --- a/natural-language-understanding/src/main/java/com/ibm/watson/developer_cloud/natural_language_understanding/v1/NaturalLanguageUnderstanding.java +++ b/natural-language-understanding/src/main/java/com/ibm/watson/developer_cloud/natural_language_understanding/v1/NaturalLanguageUnderstanding.java @@ -21,6 +21,7 @@ import com.ibm.watson.developer_cloud.natural_language_understanding.v1.model.ListModelsOptions; import com.ibm.watson.developer_cloud.natural_language_understanding.v1.model.ListModelsResults; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import com.ibm.watson.developer_cloud.util.Validator; @@ -108,6 +109,20 @@ public NaturalLanguageUnderstanding(String versionDate, String username, String setUsernameAndPassword(username, password); } + /** + * Instantiates a new `NaturalLanguageUnderstanding` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API + * calls from failing when the service introduces breaking changes. + * @param iamOptions the options for authenticating through IAM + */ + public NaturalLanguageUnderstanding(String versionDate, IamOptions iamOptions) { + this(versionDate); + setIamCredentials(iamOptions); + } + /** * Analyze text, HTML, or a public webpage. * diff --git a/personality-insights/src/main/java/com/ibm/watson/developer_cloud/personality_insights/v3/PersonalityInsights.java b/personality-insights/src/main/java/com/ibm/watson/developer_cloud/personality_insights/v3/PersonalityInsights.java index 7074d6a9c3d..bd82890a921 100644 --- a/personality-insights/src/main/java/com/ibm/watson/developer_cloud/personality_insights/v3/PersonalityInsights.java +++ b/personality-insights/src/main/java/com/ibm/watson/developer_cloud/personality_insights/v3/PersonalityInsights.java @@ -19,6 +19,7 @@ import com.ibm.watson.developer_cloud.personality_insights.v3.model.Profile; import com.ibm.watson.developer_cloud.personality_insights.v3.model.ProfileOptions; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.GsonSingleton; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import com.ibm.watson.developer_cloud.util.Validator; @@ -95,6 +96,20 @@ public PersonalityInsights(String versionDate, String username, String password) setUsernameAndPassword(username, password); } + /** + * Instantiates a new `PersonalityInsights` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API + * calls from failing when the service introduces breaking changes. + * @param iamOptions the options for authenticating through IAM + */ + public PersonalityInsights(String versionDate, IamOptions iamOptions) { + this(versionDate); + setIamCredentials(iamOptions); + } + /** * Generates a personality profile based on input text. * diff --git a/speech-to-text/src/main/java/com/ibm/watson/developer_cloud/speech_to_text/v1/SpeechToText.java b/speech-to-text/src/main/java/com/ibm/watson/developer_cloud/speech_to_text/v1/SpeechToText.java index 13562b7e2eb..0a1793cb6ce 100644 --- a/speech-to-text/src/main/java/com/ibm/watson/developer_cloud/speech_to_text/v1/SpeechToText.java +++ b/speech-to-text/src/main/java/com/ibm/watson/developer_cloud/speech_to_text/v1/SpeechToText.java @@ -17,6 +17,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.speech_to_text.v1.model.AcousticModel; import com.ibm.watson.developer_cloud.speech_to_text.v1.model.AcousticModels; import com.ibm.watson.developer_cloud.speech_to_text.v1.model.AddAudioOptions; @@ -118,6 +119,18 @@ public SpeechToText(String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `SpeechToText` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param iamOptions the options for authenticating through IAM + */ + public SpeechToText(IamOptions iamOptions) { + this(); + setIamCredentials(iamOptions); + } + /** * Retrieves information about the model. * diff --git a/text-to-speech/src/main/java/com/ibm/watson/developer_cloud/text_to_speech/v1/TextToSpeech.java b/text-to-speech/src/main/java/com/ibm/watson/developer_cloud/text_to_speech/v1/TextToSpeech.java index 9d826ff5a05..591f38f51af 100644 --- a/text-to-speech/src/main/java/com/ibm/watson/developer_cloud/text_to_speech/v1/TextToSpeech.java +++ b/text-to-speech/src/main/java/com/ibm/watson/developer_cloud/text_to_speech/v1/TextToSpeech.java @@ -16,6 +16,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.text_to_speech.v1.model.AddWordOptions; import com.ibm.watson.developer_cloud.text_to_speech.v1.model.AddWordsOptions; import com.ibm.watson.developer_cloud.text_to_speech.v1.model.CreateVoiceModelOptions; @@ -87,6 +88,18 @@ public TextToSpeech(String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `TextToSpeech` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param iamOptions the options for authenticating through IAM + */ + public TextToSpeech(IamOptions iamOptions) { + this(); + setIamCredentials(iamOptions); + } + /** * Retrieves a specific voice available for speech synthesis. * diff --git a/tone-analyzer/src/main/java/com/ibm/watson/developer_cloud/tone_analyzer/v3/ToneAnalyzer.java b/tone-analyzer/src/main/java/com/ibm/watson/developer_cloud/tone_analyzer/v3/ToneAnalyzer.java index 4d08b52bd79..3e08790635e 100644 --- a/tone-analyzer/src/main/java/com/ibm/watson/developer_cloud/tone_analyzer/v3/ToneAnalyzer.java +++ b/tone-analyzer/src/main/java/com/ibm/watson/developer_cloud/tone_analyzer/v3/ToneAnalyzer.java @@ -16,6 +16,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.tone_analyzer.v3.model.ToneAnalysis; import com.ibm.watson.developer_cloud.tone_analyzer.v3.model.ToneChatOptions; import com.ibm.watson.developer_cloud.tone_analyzer.v3.model.ToneOptions; @@ -72,6 +73,20 @@ public ToneAnalyzer(String versionDate, String username, String password) { setUsernameAndPassword(username, password); } + /** + * Instantiates a new `ToneAnalyzer` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API + * calls from failing when the service introduces breaking changes. + * @param iamOptions the options for authenticating through IAM + */ + public ToneAnalyzer(String versionDate, IamOptions iamOptions) { + this(versionDate); + setIamCredentials(iamOptions); + } + /** * Analyze general tone. * diff --git a/visual-recognition/src/main/java/com/ibm/watson/developer_cloud/visual_recognition/v3/VisualRecognition.java b/visual-recognition/src/main/java/com/ibm/watson/developer_cloud/visual_recognition/v3/VisualRecognition.java index 6ca4e72e906..1a6f511cc46 100644 --- a/visual-recognition/src/main/java/com/ibm/watson/developer_cloud/visual_recognition/v3/VisualRecognition.java +++ b/visual-recognition/src/main/java/com/ibm/watson/developer_cloud/visual_recognition/v3/VisualRecognition.java @@ -15,6 +15,7 @@ import com.ibm.watson.developer_cloud.http.RequestBuilder; import com.ibm.watson.developer_cloud.http.ServiceCall; import com.ibm.watson.developer_cloud.service.WatsonService; +import com.ibm.watson.developer_cloud.service.security.IamOptions; import com.ibm.watson.developer_cloud.util.RequestUtils; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; import com.ibm.watson.developer_cloud.util.Validator; @@ -101,6 +102,20 @@ protected void setAuthentication(okhttp3.Request.Builder builder) { } } + /** + * Instantiates a new `VisualRecognition` with IAM. Note that if the access token is specified in the iamOptions, + * you accept responsibility for managing the access token yourself. You must set a new access token before this one + * expires. Failing to do so will result in authentication errors after this token expires. + * + * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API + * calls from failing when the service introduces breaking changes. + * @param iamOptions the options for authenticating through IAM + */ + public VisualRecognition(String versionDate, IamOptions iamOptions) { + this(versionDate); + setIamCredentials(iamOptions); + } + /** * Classify images. * From 3341a8d6f1ac1e7e43353e15d5e99a489e5764e8 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Thu, 19 Apr 2018 10:22:04 -0400 Subject: [PATCH 17/26] fix(conversation): Add back method that accidentally got deleted --- .../conversation/v1/Conversation.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/conversation/src/main/java/com/ibm/watson/developer_cloud/conversation/v1/Conversation.java b/conversation/src/main/java/com/ibm/watson/developer_cloud/conversation/v1/Conversation.java index c9e9e537e8b..b7d4886125b 100644 --- a/conversation/src/main/java/com/ibm/watson/developer_cloud/conversation/v1/Conversation.java +++ b/conversation/src/main/java/com/ibm/watson/developer_cloud/conversation/v1/Conversation.java @@ -144,6 +144,45 @@ public Conversation(String versionDate, IamOptions iamOptions) { setIamCredentials(iamOptions); } + /** + * Get a response to a user's input. There is no rate limit for this operation. + * + * @param messageOptions the {@link MessageOptions} containing the options for the call + * @return a {@link ServiceCall} with a response type of {@link MessageResponse} + */ + public ServiceCall message(MessageOptions messageOptions) { + Validator.notNull(messageOptions, "messageOptions cannot be null"); + String[] pathSegments = { "v1/workspaces", "message" }; + String[] pathParameters = { messageOptions.workspaceId() }; + RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(getEndPoint(), pathSegments, + pathParameters)); + builder.query(VERSION, versionDate); + if (messageOptions.nodesVisitedDetails() != null) { + builder.query("nodes_visited_details", String.valueOf(messageOptions.nodesVisitedDetails())); + } + final JsonObject contentJson = new JsonObject(); + if (messageOptions.input() != null) { + contentJson.add("input", GsonSingleton.getGson().toJsonTree(messageOptions.input())); + } + if (messageOptions.alternateIntents() != null) { + contentJson.addProperty("alternate_intents", messageOptions.alternateIntents()); + } + if (messageOptions.context() != null) { + contentJson.add("context", GsonSingleton.getGson().toJsonTree(messageOptions.context())); + } + if (messageOptions.entities() != null) { + contentJson.add("entities", GsonSingleton.getGson().toJsonTree(messageOptions.entities())); + } + if (messageOptions.intents() != null) { + contentJson.add("intents", GsonSingleton.getGson().toJsonTree(messageOptions.intents())); + } + if (messageOptions.output() != null) { + contentJson.add("output", GsonSingleton.getGson().toJsonTree(messageOptions.output())); + } + builder.bodyJson(contentJson); + return createServiceCall(builder.build(), ResponseConverterUtils.getObject(MessageResponse.class)); + } + /** * Create workspace. * From 34577e1843faa9e4d2e379411fbfecc375012096 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Thu, 19 Apr 2018 10:44:33 -0400 Subject: [PATCH 18/26] feat(core): Add check for expired refresh token --- .../service/security/IamTokenManager.java | 34 +++++++++++++++---- .../v1/NaturalLanguageClassifier.java | 6 ++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 4489d5f9103..c5ead0708c0 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -45,7 +45,8 @@ public IamTokenManager(IamOptions options) { /** * This function returns an access token. The source of the token is determined by the following logic: * 1. If user provides their own managed access token, assume it is valid and send it - * 2. If this class is managing tokens and does not yet have one, make a request for one + * 2. If this class is managing tokens and does not yet have one, or the refresh token is expired, make a request + * for one * 3. If this class is managing tokens and the token has expired, refresh it * 4. If this class is managing tokens and has a valid token stored, send it * @@ -57,10 +58,10 @@ public String getToken() { if (userManagedAccessToken != null) { // use user-managed access token token = userManagedAccessToken; - } else if (tokenData.getAccessToken() == null) { - // request first-time token + } else if (tokenData.getAccessToken() == null || isRefreshTokenExpired()) { + // request new token token = requestToken(); - } else if (isTokenExpired()) { + } else if (isAccessTokenExpired()) { // refresh current token token = refreshToken(); } else { @@ -129,16 +130,16 @@ public void setAccessToken(String userManagedAccessToken) { } /** - * Check if currently stored token is expired. + * Check if currently stored access token is expired. * * Using a buffer to prevent the edge case of the * token expiring before the request could be made. * * The buffer will be a fraction of the total TTL. Using 80%. * - * @return whether the current managed token is expired or not + * @return whether the current managed access token is expired or not */ - private boolean isTokenExpired() { + private boolean isAccessTokenExpired() { if (tokenData.getExpiresIn() == null || tokenData.getExpiration() == null) { return true; } @@ -152,6 +153,25 @@ private boolean isTokenExpired() { return refreshTime < currentTime; } + /** + * Used as a fail-safe to prevent the condition of a refresh token expiring, + * which could happen after around 30 days. This function will return true + * if it has been at least 7 days and 1 hour since the last token was + * retrieved. + * + * @returns whether the current managed refresh token is expired or not + */ + private boolean isRefreshTokenExpired() { + if (tokenData.getExpiration() != null) { + return true; + } + + int sevenDays = 7 * 24 * 3600; + Double currentTime = Math.floor(System.currentTimeMillis() / 1000); + Long newTokenTime = tokenData.getExpiration() + sevenDays; + return newTokenTime < currentTime; + } + /** * Executes call to IAM API and returns IamToken object representing the response. * diff --git a/natural-language-classifier/src/main/java/com/ibm/watson/developer_cloud/natural_language_classifier/v1/NaturalLanguageClassifier.java b/natural-language-classifier/src/main/java/com/ibm/watson/developer_cloud/natural_language_classifier/v1/NaturalLanguageClassifier.java index 3c6fbf24dec..a75b5a5d2c2 100644 --- a/natural-language-classifier/src/main/java/com/ibm/watson/developer_cloud/natural_language_classifier/v1/NaturalLanguageClassifier.java +++ b/natural-language-classifier/src/main/java/com/ibm/watson/developer_cloud/natural_language_classifier/v1/NaturalLanguageClassifier.java @@ -77,9 +77,9 @@ public NaturalLanguageClassifier(String username, String password) { } /** - * Instantiates a new `NaturalLanguageClassifier` with IAM. Note that if the access token is specified in the iamOptions, - * you accept responsibility for managing the access token yourself. You must set a new access token before this one - * expires. Failing to do so will result in authentication errors after this token expires. + * Instantiates a new `NaturalLanguageClassifier` with IAM. Note that if the access token is specified in the + * iamOptions, you accept responsibility for managing the access token yourself. You must set a new access token + * before this one expires. Failing to do so will result in authentication errors after this token expires. * * @param iamOptions the options for authenticating through IAM */ From 33ab80befc1762f771985a3a2455175239f00029 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Thu, 19 Apr 2018 10:52:32 -0400 Subject: [PATCH 19/26] style(nlu): Fix checkstyle complaint --- .../v1/NaturalLanguageUnderstanding.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/natural-language-understanding/src/main/java/com/ibm/watson/developer_cloud/natural_language_understanding/v1/NaturalLanguageUnderstanding.java b/natural-language-understanding/src/main/java/com/ibm/watson/developer_cloud/natural_language_understanding/v1/NaturalLanguageUnderstanding.java index 7f13c510d54..d555962cc34 100644 --- a/natural-language-understanding/src/main/java/com/ibm/watson/developer_cloud/natural_language_understanding/v1/NaturalLanguageUnderstanding.java +++ b/natural-language-understanding/src/main/java/com/ibm/watson/developer_cloud/natural_language_understanding/v1/NaturalLanguageUnderstanding.java @@ -78,9 +78,9 @@ public NaturalLanguageUnderstanding(String versionDate, String username, String } /** - * Instantiates a new `NaturalLanguageUnderstanding` with IAM. Note that if the access token is specified in the iamOptions, - * you accept responsibility for managing the access token yourself. You must set a new access token before this one - * expires. Failing to do so will result in authentication errors after this token expires. + * Instantiates a new `NaturalLanguageUnderstanding` with IAM. Note that if the access token is specified in the + * iamOptions, you accept responsibility for managing the access token yourself. You must set a new access token + * before this one expires. Failing to do so will result in authentication errors after this token expires. * * @param versionDate The version date (yyyy-MM-dd) of the REST API to use. Specifying this value will keep your API * calls from failing when the service introduces breaking changes. From 25c7a7d9ada542b7932e6d5cc0e9b6baacdccca2 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Thu, 19 Apr 2018 14:09:14 -0400 Subject: [PATCH 20/26] fix(core): Fix null check in IAM logic --- .../developer_cloud/service/security/IamTokenManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index c5ead0708c0..0c7ed568b44 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -162,7 +162,7 @@ private boolean isAccessTokenExpired() { * @returns whether the current managed refresh token is expired or not */ private boolean isRefreshTokenExpired() { - if (tokenData.getExpiration() != null) { + if (tokenData.getExpiration() == null) { return true; } From f61a9a30152f491e5e110f0c08465d1cf6bad656 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Thu, 19 Apr 2018 15:51:38 -0400 Subject: [PATCH 21/26] feat(core): Pull IAM URL from VCAP_SERVICES --- .../service/WatsonService.java | 9 ++++++-- .../developer_cloud/util/CredentialUtils.java | 23 ++++++++++++++++++- core/src/test/resources/vcap_services.json | 3 ++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index 265ccfde690..bd95a011b7b 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -101,8 +101,13 @@ public abstract class WatsonService { public WatsonService(final String name) { this.name = name; String iamApiKey = CredentialUtils.getIAMKey(name); - if (iamApiKey != null) { - tokenManager = new IamTokenManager(new IamOptions.Builder().apiKey(iamApiKey).build()); + String iamUrl = CredentialUtils.getIAMUrl(name); + if (iamApiKey != null && iamUrl != null) { + IamOptions iamOptions = new IamOptions.Builder() + .apiKey(iamApiKey) + .url(iamUrl) + .build(); + tokenManager = new IamTokenManager(iamOptions); } apiKey = CredentialUtils.getAPIKey(name); String url = CredentialUtils.getAPIUrl(name); diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java b/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java index 13e469c5275..a1af0bd0018 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/util/CredentialUtils.java @@ -77,6 +77,9 @@ public String getUsername() { /** The Constant APIKEY. */ private static final String APIKEY = "apikey"; + /** The Constant IAM_API_KEY_NAME. */ + private static final String IAM_API_KEY_NAME = "iam_apikey_name"; + /** The Constant CREDENTIALS. */ private static final String CREDENTIALS = "credentials"; @@ -101,6 +104,9 @@ public String getUsername() { /** The Constant URL. */ private static final String URL = "url"; + /** The Constant IAM_URL. */ + private static final String IAM_URL = "iam_url"; + /** The Constant PLAN_EXPERIMENTAL. */ public static final String PLAN_EXPERIMENTAL = "experimental"; @@ -205,7 +211,7 @@ public static String getIAMKey(String serviceName) { } final JsonObject credentials = getCredentialsObject(services, serviceName, null); - if (credentials != null && credentials.get(APIKEY) != null && credentials.get("iam_apikey_name") != null) { + if (credentials != null && credentials.get(APIKEY) != null && credentials.get(IAM_API_KEY_NAME) != null) { return credentials.get(APIKEY).getAsString(); } @@ -369,6 +375,21 @@ public static String getAPIUrl(String serviceName, String plan) { return null; } + public static String getIAMUrl(String serviceName) { + final JsonObject services = getVCAPServices(); + + if (serviceName == null || services == null) { + return null; + } + + final JsonObject credentials = getCredentialsObject(services, serviceName, null); + if (credentials != null && credentials.get(IAM_URL) != null) { + return credentials.get(IAM_URL).getAsString(); + } + + return null; + } + /** * Sets the VCAP_SERVICES variable. This is utility variable for testing * diff --git a/core/src/test/resources/vcap_services.json b/core/src/test/resources/vcap_services.json index f4f698594f9..b9e636b10a8 100644 --- a/core/src/test/resources/vcap_services.json +++ b/core/src/test/resources/vcap_services.json @@ -70,7 +70,8 @@ "iam_apikey_name": "auto-generated-apikey-111-222-333", "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager", "iam_serviceid_crn": "crn:v1:staging:public:iam-identity::a/::serviceid:ServiceID-1234", - "url": "https://gateway.watsonplatform.net/language-translator/api" + "url": "https://gateway.watsonplatform.net/language-translator/api", + "iam_url": "https://iam.ng.bluemix.net/identity/token" } } ], From 772ae6a19c930b421b44f3a933fb2a862f5efcd8 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Thu, 19 Apr 2018 16:16:23 -0400 Subject: [PATCH 22/26] style(core): Use string constants for IAM logic --- .../service/WatsonService.java | 3 ++- .../service/security/IamTokenManager.java | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index bd95a011b7b..467f525d533 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -68,6 +68,7 @@ public abstract class WatsonService { private static final String MESSAGE_ERROR_3 = "message"; private static final String MESSAGE_ERROR_2 = "error_message"; private static final String BASIC = "Basic "; + private static final String BEARER = "Bearer "; private static final Logger LOG = Logger.getLogger(WatsonService.class.getName()); private String apiKey; private String username; @@ -294,7 +295,7 @@ public void setApiKey(String apiKey) { protected void setAuthentication(final Builder builder) { if (tokenManager != null) { String accessToken = tokenManager.getToken(); - builder.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + builder.addHeader(HttpHeaders.AUTHORIZATION, BEARER + accessToken); } else if (getApiKey() == null) { if (skipAuthentication) { return; // chosen to skip authentication with the service diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index 0c7ed568b44..b4e1ebd58c8 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -34,10 +34,18 @@ public class IamTokenManager { private IamToken tokenData; private static final String DEFAULT_AUTHORIZATION = "Basic Yng6Yng="; + private static final String DEFAULT_IAM_URL = "https://iam.ng.bluemix.net/identity/token"; + private static final String GRANT_TYPE = "grant_type"; + private static final String REQUEST_GRANT_TYPE = "urn:ibm:params:oauth:grant-type:apikey"; + private static final String REFRESH_GRANT_TYPE = "refresh_token"; + private static final String API_KEY = "apikey"; + private static final String RESPONSE_TYPE = "response_type"; + private static final String CLOUD_IAM = "cloud_iam"; + private static final String REFRESH_TOKEN = "refresh_token"; public IamTokenManager(IamOptions options) { this.apiKey = options.getApiKey(); - this.url = (options.getUrl() != null) ? options.getUrl() : "https://iam.ng.bluemix.net/identity/token"; + this.url = (options.getUrl() != null) ? options.getUrl() : DEFAULT_IAM_URL; this.userManagedAccessToken = options.getAccessToken(); tokenData = new IamToken(); } @@ -84,9 +92,9 @@ public String requestToken() { builder.header(HttpHeaders.AUTHORIZATION, DEFAULT_AUTHORIZATION); FormBody formBody = new FormBody.Builder() - .add("grant_type", "urn:ibm:params:oauth:grant-type:apikey") - .add("apikey", apiKey) - .add("response_type", "cloud_iam") + .add(GRANT_TYPE, REQUEST_GRANT_TYPE) + .add(API_KEY, apiKey) + .add(RESPONSE_TYPE, CLOUD_IAM) .build(); builder.body(formBody); @@ -106,8 +114,8 @@ public String refreshToken() { builder.header(HttpHeaders.AUTHORIZATION, DEFAULT_AUTHORIZATION); FormBody formBody = new FormBody.Builder() - .add("grant_type", "refresh_token") - .add("refresh_token", tokenData.getRefreshToken()) + .add(GRANT_TYPE, REFRESH_GRANT_TYPE) + .add(REFRESH_TOKEN, tokenData.getRefreshToken()) .build(); builder.body(formBody); From 66773bd41824c61545c5813de5fdb8c75d3d481f Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Fri, 20 Apr 2018 11:03:26 -0400 Subject: [PATCH 23/26] refactor(core): Only expose getToken method in IamTokenManager --- .../service/security/IamTokenManager.java | 18 ++---------------- .../service/IamManagerTest.java | 18 +----------------- 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java index b4e1ebd58c8..f2a2800c1f4 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -85,7 +85,7 @@ public String getToken() { * * @return the new access token */ - public String requestToken() { + private String requestToken() { RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED); @@ -107,7 +107,7 @@ public String requestToken() { * * @return the new access token */ - public String refreshToken() { + private String refreshToken() { RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED); @@ -123,20 +123,6 @@ public String refreshToken() { return tokenData.getAccessToken(); } - /** - * Set a self-managed IAM access token. - * The access token should be valid and not yet expired. - * - * By using this method, you accept responsibility for managing the access token yourself. You must set a new - * access token before this one expires. Failing to do so will result in authentication errors after this token - * expires. - * - * @param userManagedAccessToken a valid, non-expired IAM access token - */ - public void setAccessToken(String userManagedAccessToken) { - this.userManagedAccessToken = userManagedAccessToken; - } - /** * Check if currently stored access token is expired. * diff --git a/core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java b/core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java index 6ced508c169..a3990c28c7f 100644 --- a/core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java +++ b/core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java @@ -18,6 +18,7 @@ import com.ibm.watson.developer_cloud.service.security.IamTokenManager; import org.junit.Before; import org.junit.Test; + import static org.junit.Assert.assertEquals; public class IamManagerTest extends WatsonServiceUnitTest { @@ -54,23 +55,6 @@ public void getUserManagedTokenFromConstructor() { assertEquals(ACCESS_TOKEN, token); } - /** - * Tests that if a user sets their own access token at some point, that access token gets passed back during later - * retrieval. - */ - @Test - public void getUserManagedTokenFromMethodCall() { - IamOptions options = new IamOptions.Builder() - .apiKey(API_KEY) - .url(url) - .build(); - IamTokenManager manager = new IamTokenManager(options); - manager.setAccessToken(ACCESS_TOKEN); - - String token = manager.getToken(); - assertEquals(ACCESS_TOKEN, token); - } - /** * Tests that if only an API key is stored, the user can get back a valid access token. */ From 6f80223710bd2b5f0133f281af02d9b36e92f3e9 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Fri, 20 Apr 2018 11:46:40 -0400 Subject: [PATCH 24/26] docs(main README): Add documentation about using IAM --- README.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index eeb0367c2ff..ebbbdb20c84 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,10 @@ Java client library to use the [Watson APIs][wdc]. * [Gradle](#gradle) * [Usage](#usage) * [Running in IBM Cloud](#running-in-ibm-cloud) - * [Getting the Service Credentials](#getting-the-service-credentials) + * [Authentication](#authentication) + * [Username and Password](#username-and-password) + * [API Key](#api-key) + * [Using IAM](#using-iam) * IBM Watson Services * [Assistant](assistant) * [Discovery](discovery) @@ -123,17 +126,91 @@ credentials; the library will get them for you by looking at the [`VCAP_SERVICES When running in IBM Cloud (or other platforms based on Cloud Foundry), the library will automatically get the credentials from [`VCAP_SERVICES`][vcap_services]. If you have more than one plan, you can use `CredentialUtils` to get the service credentials for an specific plan. -## Getting the Service Credentials +## Authentication -You will need the `username` and `password` (`api_key` for Visual Recognition) credentials, and the API endpoint for each service. Service credentials are different from your IBM Cloud account username and password. +There are three ways to authenticate with IBM Cloud through the SDK: using a `username` and `password`, using an `api_key`, and with IAM. -To get your service credentials, follow these steps: +Getting the credentials necessary for authentication is the same process for all methods. To get them, follow these steps: 1. Log in to [IBM Cloud](https://console.bluemix.net/catalog?category=watson) 1. In the IBM Cloud **Catalog**, select the service you want to use. 1. Click **Create**. 1. On the left side of the page, click **Service Credentials**, and then **View credentials** to view your service credentials. -1. Copy `url`, `username` and `password`(`api_key` for AlchemyAPI or Visual Recognition). +1. Copy the necessary credentials (`url`, `username`, `password`, `api_key`, `apikey`, etc.). + +In your code, you can use these values in the service constructor or with a method call after instantiating your service. Here are some examples: + +### Username and Password + +```java +// in the constructor +Discovery service = new Discovery("2017-11-07", "", ""); +``` + +```java +// after instantiation +Discovery service = new Discovery("2017-11-07"); +service.setUsernameAndPassword("", ""); +``` + +### API Key + +_Note: This version of instantiation only works with Visual Recognition, as it's the only service that uses an API key rather than a username and password._ + +```java +// in the constructor +VisualRecognition service = new VisualRecognition("2016-05-20", ""); +``` + +```java +// after instantiation +VisualRecognition service = new VisualRecognition("2016-05-20"); +service.setApiKey(""); +``` + +### Using IAM + +When authenticating with IAM, you have the option of passing in the IAM API key, the IAM URL, and an IAM access token. **Be aware that passing in an access token means that you're assuming responsibility for maintaining that token's lifecycle.** If you instead pass in an IAM API key, the SDK will manage it for you. + +```java +// in the constructor, letting the SDK manage the IAM token +IamOptions options = new IamOptions.Builder() + .apiKey("") + .url("") + .build(); +Discovery service = new Discovery("2017-11-07", options); +``` + +```java +// after instantiation, letting the SDK manage the IAM token +Discovery service = new Discovery("2017-11-07"); +IamOptions options = new IamOptions.Builder() + .apiKey("") + .url("") + .build(); +service.setIamCredentials(options); +``` + +```java +// in the constructor, taking control of managing IAM token +IamOptions options = new IamOptions.Builder() + .accessToken("") + .url("") + .build(); +Discovery service = new Discovery("2017-11-07", options); +``` + +```java +// after instantiation, taking control of managing IAM token +Discovery service = new Discovery("2017-11-07"); +IamOptions options = new IamOptions.Builder() + .accessToken("") + .url("") + .build(); +service.setIamCredentials(options); +``` + +If at any time you would like to let the SDK take over managing your IAM token, simply override your stored IAM credentials with an IAM API key by calling the `setIamCredentials()` method again. ## Android From 31c42deb5bdf1235fa1d2a738eaf2060314922ac Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Fri, 20 Apr 2018 13:54:37 -0400 Subject: [PATCH 25/26] docs(main README): Address review comments --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ebbbdb20c84..e94e1a67a30 100644 --- a/README.md +++ b/README.md @@ -170,13 +170,17 @@ service.setApiKey(""); ### Using IAM -When authenticating with IAM, you have the option of passing in the IAM API key, the IAM URL, and an IAM access token. **Be aware that passing in an access token means that you're assuming responsibility for maintaining that token's lifecycle.** If you instead pass in an IAM API key, the SDK will manage it for you. +When authenticating with IAM, you have the option of passing in: +- the IAM API key and, optionally, the IAM service URL +- an IAM access token + +**Be aware that passing in an access token means that you're assuming responsibility for maintaining that token's lifecycle.** If you instead pass in an IAM API key, the SDK will manage it for you. ```java // in the constructor, letting the SDK manage the IAM token IamOptions options = new IamOptions.Builder() .apiKey("") - .url("") + .url("") // optional - the default value is https://iam.ng.bluemix.net/identity/token .build(); Discovery service = new Discovery("2017-11-07", options); ``` @@ -186,26 +190,23 @@ Discovery service = new Discovery("2017-11-07", options); Discovery service = new Discovery("2017-11-07"); IamOptions options = new IamOptions.Builder() .apiKey("") - .url("") .build(); service.setIamCredentials(options); ``` ```java -// in the constructor, taking control of managing IAM token +// in the constructor, assuming control of managing IAM token IamOptions options = new IamOptions.Builder() .accessToken("") - .url("") .build(); Discovery service = new Discovery("2017-11-07", options); ``` ```java -// after instantiation, taking control of managing IAM token +// after instantiation, assuming control of managing IAM token Discovery service = new Discovery("2017-11-07"); IamOptions options = new IamOptions.Builder() .accessToken("") - .url("") .build(); service.setIamCredentials(options); ``` From f44c5af1b67d0f48db41ae72fde00d187177c464 Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Fri, 20 Apr 2018 16:41:32 -0400 Subject: [PATCH 26/26] refactor(core): Allow tokenManager creation if IAM URL is not in VCAP_SERVICES --- .../com/ibm/watson/developer_cloud/service/WatsonService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java index 467f525d533..36e746de99c 100644 --- a/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/WatsonService.java @@ -103,7 +103,7 @@ public WatsonService(final String name) { this.name = name; String iamApiKey = CredentialUtils.getIAMKey(name); String iamUrl = CredentialUtils.getIAMUrl(name); - if (iamApiKey != null && iamUrl != null) { + if (iamApiKey != null) { IamOptions iamOptions = new IamOptions.Builder() .apiKey(iamApiKey) .url(iamUrl)