diff --git a/README.md b/README.md index eeb0367c2ff..e94e1a67a30 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,92 @@ 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 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("") // optional - the default value is https://iam.ng.bluemix.net/identity/token + .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("") + .build(); +service.setIamCredentials(options); +``` + +```java +// in the constructor, assuming control of managing IAM token +IamOptions options = new IamOptions.Builder() + .accessToken("") + .build(); +Discovery service = new Discovery("2017-11-07", options); +``` + +```java +// after instantiation, assuming control of managing IAM token +Discovery service = new Discovery("2017-11-07"); +IamOptions options = new IamOptions.Builder() + .accessToken("") + .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 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 1bccf2af3b4..5cda507382d 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; @@ -130,8 +131,20 @@ public Assistant(String versionDate, String username, String password) { } /** - * Get response to user input. + * 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. * * @param messageOptions the {@link MessageOptions} containing the options for the call 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 6b3bb2a2a3a..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 @@ -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; @@ -130,8 +131,20 @@ public Conversation(String versionDate, String username, String password) { } /** - * Get response to user input. + * 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); + } + + /** * 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 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..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 @@ -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.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; import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; @@ -66,12 +68,14 @@ 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; private String password; private String endPoint; private final String name; + private IamTokenManager tokenManager; private OkHttpClient client; @@ -97,6 +101,15 @@ public abstract class WatsonService { */ public WatsonService(final String name) { this.name = name; + String iamApiKey = CredentialUtils.getIAMKey(name); + String iamUrl = CredentialUtils.getIAMUrl(name); + if (iamApiKey != null) { + IamOptions iamOptions = new IamOptions.Builder() + .apiKey(iamApiKey) + .url(iamUrl) + .build(); + tokenManager = new IamTokenManager(iamOptions); + } apiKey = CredentialUtils.getAPIKey(name); String url = CredentialUtils.getAPIUrl(name); if ((url != null) && !url.isEmpty()) { @@ -280,13 +293,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 +342,19 @@ 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 + */ + public void setIamCredentials(IamOptions iamOptions) { + this.tokenManager = new IamTokenManager(iamOptions); + } + /* * (non-Javadoc) * 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 new file mode 100644 index 00000000000..3aa34f43027 --- /dev/null +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamOptions.java @@ -0,0 +1,65 @@ +/* + * 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; + +/** + * Options for authenticating using IAM. + */ +public class IamOptions { + private String apiKey; + private String accessToken; + private String url; + + public String getApiKey() { + return apiKey; + } + + public String getAccessToken() { + return accessToken; + } + + public String getUrl() { + return url; + } + + public static class Builder { + private String apiKey; + private String accessToken; + private String url; + + public IamOptions build() { + return new IamOptions(this); + } + + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public Builder accessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public Builder url(String url) { + this.url = url; + return this; + } + } + + private IamOptions(Builder builder) { + this.apiKey = builder.apiKey; + this.accessToken = builder.accessToken; + 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 new file mode 100644 index 00000000000..ed09149863a --- /dev/null +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamToken.java @@ -0,0 +1,51 @@ +/* + * 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. + */ +public 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; + + 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..f2a2800c1f4 --- /dev/null +++ b/core/src/main/java/com/ibm/watson/developer_cloud/service/security/IamTokenManager.java @@ -0,0 +1,186 @@ +/* + * 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.ResponseConverter; +import com.ibm.watson.developer_cloud.util.ResponseConverterUtils; +import okhttp3.Call; +import okhttp3.FormBody; +import okhttp3.Request; + +import java.io.IOException; + +/** + * 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="; + 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() : DEFAULT_IAM_URL; + this.userManagedAccessToken = options.getAccessToken(); + tokenData = new IamToken(); + } + + /** + * 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, 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 + * + * @return the valid access token + */ + public String getToken() { + String token; + + if (userManagedAccessToken != null) { + // use user-managed access token + token = userManagedAccessToken; + } else if (tokenData.getAccessToken() == null || isRefreshTokenExpired()) { + // request new token + token = requestToken(); + } else if (isAccessTokenExpired()) { + // refresh current token + token = refreshToken(); + } else { + // use valid managed token + token = tokenData.getAccessToken(); + } + + return token; + } + + /** + * Request an IAM token using an API key. Also updates internal managed IAM token information. + * + * @return the new access token + */ + private String requestToken() { + RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); + + builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED); + builder.header(HttpHeaders.AUTHORIZATION, DEFAULT_AUTHORIZATION); + + FormBody formBody = new FormBody.Builder() + .add(GRANT_TYPE, REQUEST_GRANT_TYPE) + .add(API_KEY, apiKey) + .add(RESPONSE_TYPE, CLOUD_IAM) + .build(); + builder.body(formBody); + + tokenData = callIamApi(builder.build()); + return tokenData.getAccessToken(); + } + + /** + * Refresh an IAM token using a refresh token. Also updates internal managed IAM token information. + * + * @return the new access token + */ + private String refreshToken() { + RequestBuilder builder = RequestBuilder.post(RequestBuilder.constructHttpUrl(url, new String[0])); + + builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_FORM_URLENCODED); + builder.header(HttpHeaders.AUTHORIZATION, DEFAULT_AUTHORIZATION); + + FormBody formBody = new FormBody.Builder() + .add(GRANT_TYPE, REFRESH_GRANT_TYPE) + .add(REFRESH_TOKEN, tokenData.getRefreshToken()) + .build(); + builder.body(formBody); + + tokenData = callIamApi(builder.build()); + return tokenData.getAccessToken(); + } + + /** + * 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 access token is expired or not + */ + private boolean isAccessTokenExpired() { + 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 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. + * + * @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); + } + } +} 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..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"; @@ -191,6 +197,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 getIAMKey(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_API_KEY_NAME) != null) { + return credentials.get(APIKEY).getAsString(); + } + + return null; + } + /** * Returns the apiKey from the VCAP_SERVICES or null if doesn't exists. * @@ -348,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/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..a3990c28c7f --- /dev/null +++ b/core/src/test/java/com/ibm/watson/developer_cloud/service/IamManagerTest.java @@ -0,0 +1,97 @@ +/** + * 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 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 5a2d6ff8e29..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,20 +12,19 @@ */ package com.ibm.watson.developer_cloud.util; -import static org.junit.Assert.assertEquals; -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. @@ -54,6 +53,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 +127,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/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 diff --git a/core/src/test/resources/vcap_services.json b/core/src/test/resources/vcap_services.json index 2b3cdeea9c1..b9e636b10a8 100644 --- a/core/src/test/resources/vcap_services.json +++ b/core/src/test/resources/vcap_services.json @@ -59,15 +59,19 @@ } } ], - "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", + "iam_url": "https://iam.ng.bluemix.net/identity/token" } } ], 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 793f937109d..473f6e04d27 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; @@ -71,6 +72,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..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 @@ -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 211b71b0837..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 @@ -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; @@ -77,7 +78,23 @@ public NaturalLanguageUnderstanding(String versionDate, String username, String } /** - * Analyze text, HTML, or a public webpage with one or more text analysis features. + * 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. + * + * Analyzes text, HTML, or a public webpage with one or more text analysis features. * * @param analyzeOptions the {@link AnalyzeOptions} containing the options for the call * @return a {@link ServiceCall} with a response type of {@link AnalysisResults} 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 1f198c5d735..3dadc36b243 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; @@ -79,7 +80,21 @@ public PersonalityInsights(String versionDate, String username, String password) } /** - * Get profile. + * 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. * * Generates a personality profile for the author of the input text. The service accepts a maximum of 20 MB of input * content, but it requires much less text to produce an accurate profile; for more information, see [Providing 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 c5a290ae418..db18bbfabf4 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; @@ -120,7 +121,19 @@ public SpeechToText(String username, String password) { } /** - * Get a model. + * 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. * * Retrieves information about a single specified language model that is available for use with the service. The * information includes the name of the model and its minimum sampling rate in Hertz, among other things. 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 3d92a01382b..9028d3d0430 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; @@ -89,7 +90,19 @@ public TextToSpeech(String username, String password) { } /** - * Get a voice. + * 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. * * Lists information about the specified voice. The information includes the name, language, gender, and other details * about the voice. Specify a customization ID to obtain information for that custom voice model of the specified 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. *